From d9f60a43c7699f075e90af1953ad3025a5921553 Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 20:07:05 +0300 Subject: [PATCH 01/67] feat: read both API response shapes; never send an empty webhook_url on verify Every model, the webhook parser, the error mapping and the CLI read the API's newer response shape as well as the original one. The newer field names are attributes (ExtractedClaims.claims, AssessClaim.status/failure/ more_claims, TaskStatus.failure, completed_at, claim, FailureBlock.code, the *_limit_exceeded flags, webhook event_id), and every older attribute keeps its original meaning, computed from whichever shape arrives. model_dump() returns the shape the server sent. verify and verify_batch leave webhook_url out instead of sending "" (both meant the key's default); review and citecheck bodies are unchanged. The /me/usage deprecations no longer name a removal date. Parity tests freeze what the previous release produced from each original-shape response (models, CLI text and JSON, wait outcomes, errors, webhooks, request bodies) and hold both shapes to it. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 76 ++ README.md | 79 +- openapi.json | 8 +- src/lenz_io/__init__.py | 12 +- src/lenz_io/cli/render.py | 30 +- src/lenz_io/cli/verify.py | 4 +- src/lenz_io/client.py | 62 +- src/lenz_io/errors.py | 37 +- src/lenz_io/models.py | 805 +++++++++++++++--- src/lenz_io/webhooks.py | 239 +++--- .../canonical/account__library_empty.json | 14 + ...dified_at_crosses_midnight_by_minutes.json | 30 + ...item_modified_at_same_day_hours_apart.json | 30 + .../canonical/account__library_items.json | 60 ++ .../canonical/account__library_no_match.json | 14 + .../account__library_page_past_end.json | 14 + ...ccount__library_paginated_second_page.json | 45 + .../account__me_usage_extra_credits.json | 40 + .../account__me_usage_extra_only.json | 40 + .../account__me_usage_extract_calls.json | 40 + .../canonical/account__me_usage_free.json | 40 + .../account__me_usage_free_partly_spent.json | 40 + .../canonical/account__me_usage_plus.json | 40 + .../canonical/account__me_usage_pro.json | 40 + .../account__me_usage_pro_extra.json | 40 + .../account__me_usage_stale_quota_row.json | 40 + .../parity/canonical/assess__401_no_key.json | 12 + .../canonical/assess__402_no_credits.json | 19 + .../canonical/assess__422_blank_item.json | 23 + .../canonical/assess__422_blank_text.json | 22 + .../canonical/assess__422_input_conflict.json | 22 + .../canonical/assess__422_item_too_long.json | 23 + .../canonical/assess__422_no_input_field.json | 23 + .../canonical/assess__422_too_many_items.json | 22 + .../assess__422_wrong_type_claim.json | 23 + .../assess__422_wrong_type_claims.json | 23 + .../canonical/assess__502_framing_failed.json | 12 + .../canonical/assess__503_capacity.json | 15 + ...ss__503_list_all_upstream_unavailable.json | 15 + .../assess__503_upstream_unavailable.json | 15 + .../canonical/assess__deep_tier_hit.json | 28 + .../assess__idempotency_body_mismatch.json | 12 + .../canonical/assess__idempotency_first.json | 28 + .../canonical/assess__idempotency_replay.json | 28 + .../assess__list_all_error_rows.json | 55 ++ .../canonical/assess__list_compound_item.json | 44 + .../canonical/assess__list_mixed_rows.json | 88 ++ .../parity/canonical/assess__memo_hit.json | 28 + .../canonical/assess__replay_later_200.json | 28 + .../canonical/assess__single_no_claim.json | 21 + .../canonical/assess__single_one_claim.json | 28 + ...ss__single_text_over_wave_more_claims.json | 278 ++++++ .../assess__single_text_several_claims.json | 54 ++ .../canonical/assess__suggest_rewrite.json | 41 + .../canonical/citecheck__402_no_credits.json | 18 + .../canonical/citecheck__422_empty_body.json | 25 + .../canonical/citecheck__422_invalid_doi.json | 25 + .../citecheck__422_max_citations_over_20.json | 26 + ...techeck__422_max_citations_with_pairs.json | 25 + .../citecheck__422_pair_bad_url_scheme.json | 28 + .../citecheck__422_pair_no_source.json | 27 + .../citecheck__422_pair_url_and_doi.json | 27 + .../canonical/citecheck__422_pairs_empty.json | 26 + .../citecheck__422_pairs_over_20.json | 26 + ...citecheck__422_quote_not_in_statement.json | 24 + .../citecheck__422_text_and_pairs.json | 25 + .../citecheck__422_unknown_field.json | 25 + .../canonical/citecheck__422_url_input.json | 22 + .../citecheck__429_citecheck_in_flight.json | 14 + .../canonical/citecheck__503_capacity.json | 15 + .../citecheck__503_citations_unavailable.json | 15 + .../citecheck__get_404_not_found.json | 12 + .../citecheck__get_404_review_id.json | 12 + .../canonical/citecheck__get_410_purged.json | 13 + .../canonical/citecheck__get_checking.json | 103 +++ .../citecheck__get_completed_clean.json | 111 +++ .../citecheck__get_completed_doi_pair.json | 76 ++ .../citecheck__get_completed_incomplete.json | 129 +++ ...citecheck__get_completed_issues_found.json | 141 +++ ...check__get_completed_partly_supported.json | 111 +++ .../citecheck__get_completed_unchecked.json | 111 +++ .../citecheck__get_failed_no_citations.json | 43 + ...heck__get_failed_upstream_unavailable.json | 153 ++++ .../canonical/citecheck__get_queued.json | 72 ++ ...heck__get_text_limit_exactly_at_limit.json | 119 +++ .../citecheck__get_text_limit_reached.json | 93 ++ ...echeck__idempotency_body_mismatch_422.json | 12 + .../citecheck__idempotency_conflict_409.json | 13 + .../citecheck__idempotent_replay_202.json | 14 + .../canonical/citecheck__receipt_202.json | 14 + ...echeck__receipt_202_empty_webhook_url.json | 14 + .../canonical/errors__ask_bad_language.json | 22 + .../canonical/errors__ask_empty_message.json | 22 + .../parity/canonical/errors__ask_failed.json | 12 + .../errors__ask_idempotency_key_reused.json | 12 + .../errors__ask_message_too_long.json | 22 + .../canonical/errors__ask_not_completed.json | 12 + .../errors__ask_reset_not_found.json | 12 + .../canonical/errors__ask_send_not_found.json | 12 + .../canonical/errors__auth_bad_key.json | 12 + .../canonical/errors__auth_basic_scheme.json | 12 + .../errors__auth_insufficient_scope.json | 14 + .../errors__auth_missing_bearer.json | 12 + .../errors__auth_missing_bearer_post.json | 12 + ...rrors__auth_private_claim_ask_history.json | 12 + .../errors__auth_private_claim_ask_send.json | 12 + .../errors__auth_unrecognised_prefix.json | 12 + ...rrors__http_error_library_bad_curated.json | 22 + ...rrors__http_error_library_bad_verdict.json | 22 + .../canonical/errors__not_found_ask.json | 12 + .../canonical/errors__not_found_route.json | 12 + .../errors__not_found_verification.json | 12 + .../errors__not_found_verify_status.json | 12 + .../errors__payment_required_ask.json | 19 + .../errors__payment_required_assess.json | 19 + .../errors__payment_required_verify.json | 19 + ...__payment_required_verify_batch_short.json | 20 + .../errors__rate_limited_extract.json | 17 + .../errors__service_unavailable_ask.json | 15 + .../errors__service_unavailable_capacity.json | 15 + .../errors__validation_malformed_json.json | 12 + ...errors__validation_missing_field_hint.json | 29 + ...errors__validation_unknown_field_only.json | 28 + ...__validation_unknown_field_wrong_type.json | 27 + .../errors__validation_wrong_type.json | 23 + .../parity/canonical/extract__401_no_key.json | 12 + .../canonical/extract__422_blank_text.json | 22 + .../extract__422_focus_too_long.json | 22 + .../canonical/extract__422_missing_text.json | 23 + .../canonical/extract__429_daily_limit.json | 17 + .../extract__502_extraction_failed.json | 12 + .../extract__503_upstream_unavailable.json | 15 + .../canonical/extract__focus_ready.json | 30 + ...xtract__idempotency_body_mismatch_422.json | 12 + .../extract__idempotency_replay.json | 34 + .../extract__locate_all_dropped.json | 25 + .../canonical/extract__locate_failed.json | 34 + .../extract__locate_leading_whitespace.json | 36 + .../extract__locate_some_dropped.json | 46 + .../canonical/extract__locate_true.json | 46 + .../extract__no_match_with_focus.json | 16 + .../canonical/extract__not_a_claim.json | 16 + .../extract__not_a_claim_beside_claims.json | 30 + .../canonical/extract__ready_one_claim.json | 30 + .../extract__ready_several_claims.json | 34 + .../canonical/extract__replay_later_200.json | 31 + .../extract__url_fetch_502_unreadable.json | 12 + .../extract__url_fetch_503_upstream.json | 15 + .../canonical/extract__url_input_located.json | 36 + .../canonical/review__401_no_credentials.json | 12 + .../canonical/review__402_no_credits.json | 19 + .../review__402_no_credits_exhausted.json | 19 + .../canonical/review__422_blank_text.json | 22 + .../review__422_citations_options_object.json | 23 + .../canonical/review__422_depth_unknown.json | 27 + .../review__422_invalid_confidence_band.json | 27 + .../review__422_invalid_verdict_label.json | 28 + .../review__422_max_assessments_negative.json | 27 + .../review__422_max_citations_negative.json | 27 + .../review__422_max_citations_too_big.json | 27 + ...review__422_max_verifications_too_big.json | 27 + .../canonical/review__422_text_missing.json | 23 + .../review__422_text_wrong_type.json | 23 + .../review__422_unknown_escalate_field.json | 24 + .../review__422_unknown_top_level_field.json | 23 + .../review__422_unsupported_language.json | 22 + .../review__422_visibility_public.json | 26 + .../review__429_extract_daily_limit.json | 17 + .../review__429_review_in_flight.json | 14 + .../canonical/review__503_capacity.json | 15 + .../review__503_citations_capacity.json | 15 + .../review__get_403_insufficient_scope.json | 14 + .../canonical/review__get_404_not_found.json | 12 + .../review__get_404_other_account.json | 12 + .../canonical/review__get_410_purged.json | 13 + .../review__get_422_unknown_view.json | 22 + .../canonical/review__get_assessing.json | 107 +++ ...view__get_assessment_rows_full_fields.json | 169 ++++ .../review__get_citations_clean.json | 182 ++++ ...w__get_citations_failed_and_unchecked.json | 200 +++++ .../review__get_citations_issue_full.json | 204 +++++ ...view__get_citations_issue_issues_view.json | 94 ++ ..._get_citations_limit_exactly_at_limit.json | 182 ++++ .../review__get_citations_limit_reached.json | 156 ++++ ..._get_citations_no_claim_but_citations.json | 153 ++++ ...w__get_citations_no_claim_no_citation.json | 70 ++ .../review__get_citations_not_asked.json | 99 +++ .../canonical/review__get_citations_only.json | 160 ++++ ...eview__get_citations_partly_supported.json | 182 ++++ .../review__get_citations_running.json | 166 ++++ ...w__get_citations_skipped_switched_off.json | 99 +++ ...view__get_citations_skipped_url_input.json | 70 ++ .../review__get_claim_limit_not_reached.json | 127 +++ ...review__get_claim_limit_reached_exact.json | 127 +++ .../review__get_completed_assess_only.json | 153 ++++ .../review__get_completed_clean.json | 127 +++ ...ew__get_completed_issues_all_verified.json | 256 ++++++ .../review__get_completed_issues_full.json | 309 +++++++ ...iew__get_completed_issues_view_issues.json | 136 +++ .../review__get_completed_positions.json | 139 +++ ...review__get_completed_recovered_claim.json | 99 +++ ...ew__get_completed_with_untraced_claim.json | 139 +++ .../review__get_during_quick_check.json | 87 ++ ...w__get_failed_every_assessment_failed.json | 159 ++++ .../review__get_failed_no_claim.json | 70 ++ .../review__get_failed_no_claim_untraced.json | 70 ++ .../canonical/review__get_more_claims.json | 152 ++++ ...dified_at_crosses_midnight_by_minutes.json | 150 ++++ ...tion_modified_at_same_day_hours_apart.json | 150 ++++ .../review__get_policy_resolved.json | 97 +++ .../parity/canonical/review__get_queued.json | 63 ++ .../review__get_suggested_edits.json | 266 ++++++ ...view__get_suggested_edits_issues_view.json | 129 +++ .../canonical/review__get_url_review.json | 105 +++ .../canonical/review__get_verifying.json | 176 ++++ ...review__idempotency_body_mismatch_422.json | 12 + .../review__idempotency_conflict_409.json | 13 + ...mpotency_conflict_409_existing_review.json | 13 + .../review__idempotent_replay_202.json | 14 + .../parity/canonical/review__receipt_202.json | 14 + ...review__receipt_202_empty_webhook_url.json | 14 + .../review__receipt_202_webhook_url.json | 14 + .../canonical/review__replay_later_202.json | 14 + .../parity/canonical/verify__batch_202.json | 21 + .../canonical/verify__batch_capacity_503.json | 15 + .../canonical/verify__batch_empty_422.json | 22 + ...erify__batch_idempotency_conflict_409.json | 13 + .../verify__batch_item_missing_claim_422.json | 25 + .../verify__batch_no_credits_402.json | 20 + .../canonical/verify__batch_partial_202.json | 18 + .../canonical/verify__batch_too_many_422.json | 22 + .../canonical/verify__blank_claim_422.json | 22 + .../canonical/verify__capacity_503.json | 15 + ...verify__idempotency_body_mismatch_422.json | 12 + .../verify__idempotency_conflict_409.json | 13 + .../verify__idempotency_key_replay.json | 12 + .../canonical/verify__implicit_dedup_200.json | 12 + .../verify__implicit_repeat_replay.json | 12 + .../canonical/verify__invalid_depth_422.json | 26 + .../verify__invalid_language_422.json | 22 + .../parity/canonical/verify__list_200.json | 45 + ...dified_at_crosses_midnight_by_minutes.json | 30 + ..._200_modified_at_same_day_hours_apart.json | 30 + .../canonical/verify__list_clamped_200.json | 14 + .../canonical/verify__list_empty_200.json | 14 + .../canonical/verify__list_page2_200.json | 30 + .../canonical/verify__misnamed_field_422.json | 28 + .../canonical/verify__missing_claim_422.json | 23 + .../canonical/verify__no_credits_402.json | 19 + .../canonical/verify__replay_later_202.json | 13 + .../parity/canonical/verify__select_202.json | 21 + .../canonical/verify__select_empty_422.json | 22 + .../verify__select_invalid_selection_422.json | 22 + ...rify__select_no_selection_pending_409.json | 12 + .../verify__select_too_many_422.json | 22 + .../verify__select_unknown_task_404.json | 12 + ...erify__status_cancelled_after_a_while.json | 20 + .../canonical/verify__status_completed.json | 99 +++ ...erify__status_completed_after_a_while.json | 99 +++ ...dified_at_crosses_midnight_by_minutes.json | 99 +++ ...hile_modified_at_same_day_hours_apart.json | 99 +++ ...dified_at_crosses_midnight_by_minutes.json | 99 +++ ...eted_modified_at_same_day_hours_apart.json | 99 +++ .../canonical/verify__status_failed.json | 20 + .../verify__status_failed_after_a_while.json | 20 + ...__status_failed_after_a_while_framing.json | 20 + .../verify__status_failed_retryable.json | 20 + .../canonical/verify__status_needs_input.json | 24 + .../canonical/verify__status_not_a_claim.json | 20 + ...ify__status_not_a_claim_after_a_while.json | 20 + .../verify__status_not_yours_404.json | 12 + .../verify__status_older_run_completed.json | 99 +++ ...rify__status_older_run_failed_crashed.json | 20 + ...lder_run_failed_insufficient_evidence.json | 20 + .../verify__status_older_run_in_progress.json | 19 + .../canonical/verify__status_processing.json | 19 + ...rify__status_processing_after_a_while.json | 19 + .../canonical/verify__status_task_stuck.json | 20 + .../canonical/verify__status_unknown_404.json | 12 + .../parity/canonical/verify__submit_202.json | 12 + .../canonical/verify__submit_202_options.json | 12 + .../verify__submit_202_text_alias.json | 12 + .../verify__unauthenticated_401.json | 12 + .../canonical/verify__verification_200.json | 95 +++ .../verify__verification_200_covered.json | 93 ++ ...dified_at_crosses_midnight_by_minutes.json | 95 +++ ...fy__verification_200_modified_at_null.json | 95 +++ ..._200_modified_at_same_day_hours_apart.json | 95 +++ ...ify__verification_200_modified_at_set.json | 95 +++ ...ation_200_result_json_conclusion_only.json | 56 ++ ...y__verification_200_result_json_empty.json | 56 ++ ...ion_200_result_json_old_shape_strings.json | 72 ++ .../canonical/verify__verification_404.json | 12 + .../verify__verification_by_task_id_200.json | 95 +++ .../verify__verification_failed_409.json | 23 + ..._failed_409_not_a_claim_after_a_while.json | 23 + .../verify__verification_not_ready_409.json | 15 + .../verify__verification_not_yours_404.json | 12 + ...verify__verification_public_anonymous.json | 83 ++ ...__verification_public_of_another_user.json | 95 +++ ...verify__verification_unknown_task_404.json | 13 + ...rification_zero_retention_expired_410.json | 13 + ...y__verification_zero_retention_served.json | 95 +++ .../webhook__certificate_timestamped.json | 25 + ..._certificate_timestamped_review_child.json | 26 + .../webhook__citecheck_completed.json | 133 +++ .../canonical/webhook__citecheck_failed.json | 46 + .../canonical/webhook__review_completed.json | 155 ++++ ...ook__review_completed_key_default_url.json | 104 +++ ...k__review_completed_oversized_rebuilt.json | 10 + .../canonical/webhook__review_failed.json | 72 ++ .../webhook__verification_completed.json | 109 +++ ...hook__verification_completed_in_batch.json | 109 +++ ...dified_at_crosses_midnight_by_minutes.json | 109 +++ ...eted_modified_at_same_day_hours_apart.json | 109 +++ ...fication_failed_insufficient_evidence.json | 26 + ...hook__verification_failed_not_a_claim.json | 26 + ...ification_failed_upstream_unavailable.json | 26 + ..._verification_needs_input_multi_claim.json | 30 + .../expected/account__library_empty.json | 9 + ...dified_at_crosses_midnight_by_minutes.json | 25 + ...item_modified_at_same_day_hours_apart.json | 25 + .../expected/account__library_items.json | 55 ++ .../expected/account__library_no_match.json | 9 + .../account__library_page_past_end.json | 9 + ...ccount__library_paginated_second_page.json | 40 + .../account__me_usage_extra_credits.json | 62 ++ .../account__me_usage_extra_only.json | 62 ++ .../account__me_usage_extract_calls.json | 62 ++ .../expected/account__me_usage_free.json | 62 ++ .../account__me_usage_free_partly_spent.json | 62 ++ .../expected/account__me_usage_plus.json | 62 ++ .../expected/account__me_usage_pro.json | 62 ++ .../expected/account__me_usage_pro_extra.json | 62 ++ .../account__me_usage_stale_quota_row.json | 62 ++ .../parity/expected/assess__401_no_key.json | 21 + .../expected/assess__402_no_credits.json | 28 + .../expected/assess__422_blank_item.json | 22 + .../expected/assess__422_blank_text.json | 22 + .../expected/assess__422_input_conflict.json | 22 + .../expected/assess__422_item_too_long.json | 22 + .../expected/assess__422_no_input_field.json | 32 + .../expected/assess__422_too_many_items.json | 22 + .../assess__422_wrong_type_claim.json | 32 + .../assess__422_wrong_type_claims.json | 32 + .../expected/assess__502_framing_failed.json | 22 + .../parity/expected/assess__503_capacity.json | 22 + ...ss__503_list_all_upstream_unavailable.json | 22 + .../assess__503_upstream_unavailable.json | 22 + .../expected/assess__deep_tier_hit.json | 26 + .../assess__idempotency_body_mismatch.json | 22 + .../expected/assess__idempotency_first.json | 26 + .../expected/assess__idempotency_replay.json | 26 + .../expected/assess__list_all_error_rows.json | 40 + .../expected/assess__list_compound_item.json | 43 + .../expected/assess__list_mixed_rows.json | 68 ++ .../parity/expected/assess__memo_hit.json | 26 + .../expected/assess__replay_later_200.json | 26 + .../expected/assess__single_no_claim.json | 11 + .../expected/assess__single_one_claim.json | 26 + ...ss__single_text_over_wave_more_claims.json | 295 +++++++ .../assess__single_text_several_claims.json | 54 ++ .../expected/assess__suggest_rewrite.json | 40 + .../expected/citecheck__402_no_credits.json | 28 + .../expected/citecheck__422_empty_body.json | 30 + .../expected/citecheck__422_invalid_doi.json | 33 + .../citecheck__422_max_citations_over_20.json | 31 + ...techeck__422_max_citations_with_pairs.json | 30 + .../citecheck__422_pair_bad_url_scheme.json | 33 + .../citecheck__422_pair_no_source.json | 32 + .../citecheck__422_pair_url_and_doi.json | 32 + .../expected/citecheck__422_pairs_empty.json | 31 + .../citecheck__422_pairs_over_20.json | 31 + ...citecheck__422_quote_not_in_statement.json | 32 + .../citecheck__422_text_and_pairs.json | 30 + .../citecheck__422_unknown_field.json | 33 + .../expected/citecheck__422_url_input.json | 30 + .../citecheck__429_citecheck_in_flight.json | 25 + .../expected/citecheck__503_capacity.json | 22 + .../citecheck__503_citations_unavailable.json | 22 + .../citecheck__get_404_not_found.json | 21 + .../citecheck__get_404_review_id.json | 21 + .../expected/citecheck__get_410_purged.json | 22 + .../expected/citecheck__get_checking.json | 100 +++ .../citecheck__get_completed_clean.json | 108 +++ .../citecheck__get_completed_doi_pair.json | 73 ++ .../citecheck__get_completed_incomplete.json | 124 +++ ...citecheck__get_completed_issues_found.json | 138 +++ ...check__get_completed_partly_supported.json | 108 +++ .../citecheck__get_completed_unchecked.json | 108 +++ .../citecheck__get_failed_no_citations.json | 39 + ...heck__get_failed_upstream_unavailable.json | 145 ++++ .../expected/citecheck__get_queued.json | 69 ++ ...heck__get_text_limit_exactly_at_limit.json | 116 +++ .../citecheck__get_text_limit_reached.json | 90 ++ ...echeck__idempotency_body_mismatch_422.json | 29 + .../citecheck__idempotency_conflict_409.json | 21 + .../citecheck__idempotent_replay_202.json | 7 + .../expected/citecheck__receipt_202.json | 7 + ...echeck__receipt_202_empty_webhook_url.json | 7 + .../expected/errors__ask_bad_language.json | 22 + .../expected/errors__ask_empty_message.json | 22 + .../parity/expected/errors__ask_failed.json | 22 + .../errors__ask_idempotency_key_reused.json | 22 + .../errors__ask_message_too_long.json | 22 + .../expected/errors__ask_not_completed.json | 21 + .../expected/errors__ask_reset_not_found.json | 21 + .../expected/errors__ask_send_not_found.json | 21 + .../parity/expected/errors__auth_bad_key.json | 21 + .../expected/errors__auth_basic_scheme.json | 21 + .../errors__auth_insufficient_scope.json | 21 + .../expected/errors__auth_missing_bearer.json | 21 + .../errors__auth_missing_bearer_post.json | 21 + ...rrors__auth_private_claim_ask_history.json | 21 + .../errors__auth_private_claim_ask_send.json | 21 + .../errors__auth_unrecognised_prefix.json | 21 + ...rrors__http_error_library_bad_curated.json | 22 + ...rrors__http_error_library_bad_verdict.json | 22 + .../expected/errors__not_found_ask.json | 21 + .../expected/errors__not_found_route.json | 21 + .../errors__not_found_verification.json | 21 + .../errors__not_found_verify_status.json | 21 + .../errors__payment_required_ask.json | 28 + .../errors__payment_required_assess.json | 28 + .../errors__payment_required_verify.json | 28 + ...__payment_required_verify_batch_short.json | 28 + .../errors__rate_limited_extract.json | 25 + .../errors__service_unavailable_ask.json | 22 + .../errors__service_unavailable_capacity.json | 22 + .../errors__validation_malformed_json.json | 21 + ...errors__validation_missing_field_hint.json | 32 + ...errors__validation_unknown_field_only.json | 32 + ...__validation_unknown_field_wrong_type.json | 32 + .../errors__validation_wrong_type.json | 32 + .../parity/expected/extract__401_no_key.json | 21 + .../expected/extract__422_blank_text.json | 22 + .../expected/extract__422_focus_too_long.json | 22 + .../expected/extract__422_missing_text.json | 32 + .../expected/extract__429_daily_limit.json | 25 + .../extract__502_extraction_failed.json | 22 + .../extract__503_upstream_unavailable.json | 22 + .../parity/expected/extract__focus_ready.json | 24 + ...xtract__idempotency_body_mismatch_422.json | 22 + .../expected/extract__idempotency_replay.json | 27 + .../expected/extract__locate_all_dropped.json | 24 + .../expected/extract__locate_failed.json | 27 + .../extract__locate_leading_whitespace.json | 35 + .../extract__locate_some_dropped.json | 48 ++ .../parity/expected/extract__locate_true.json | 48 ++ .../extract__no_match_with_focus.json | 15 + .../parity/expected/extract__not_a_claim.json | 15 + .../extract__not_a_claim_beside_claims.json | 24 + .../expected/extract__ready_one_claim.json | 24 + .../extract__ready_several_claims.json | 27 + .../expected/extract__replay_later_200.json | 27 + .../extract__url_fetch_502_unreadable.json | 22 + .../extract__url_fetch_503_upstream.json | 22 + .../expected/extract__url_input_located.json | 35 + .../expected/review__401_no_credentials.json | 21 + .../expected/review__402_no_credits.json | 28 + .../review__402_no_credits_exhausted.json | 28 + .../expected/review__422_blank_text.json | 30 + .../review__422_citations_options_object.json | 31 + .../expected/review__422_depth_unknown.json | 32 + .../review__422_invalid_confidence_band.json | 32 + .../review__422_invalid_verdict_label.json | 32 + .../review__422_max_assessments_negative.json | 32 + .../review__422_max_citations_negative.json | 32 + .../review__422_max_citations_too_big.json | 32 + ...review__422_max_verifications_too_big.json | 32 + .../expected/review__422_text_missing.json | 31 + .../expected/review__422_text_wrong_type.json | 31 + .../review__422_unknown_escalate_field.json | 32 + .../review__422_unknown_top_level_field.json | 31 + .../review__422_unsupported_language.json | 30 + .../review__422_visibility_public.json | 31 + .../review__429_extract_daily_limit.json | 25 + .../review__429_review_in_flight.json | 25 + .../parity/expected/review__503_capacity.json | 22 + .../review__503_citations_capacity.json | 22 + .../review__get_403_insufficient_scope.json | 21 + .../expected/review__get_404_not_found.json | 21 + .../review__get_404_other_account.json | 21 + .../expected/review__get_410_purged.json | 22 + .../review__get_422_unknown_view.json | 30 + .../expected/review__get_assessing.json | 108 +++ ...view__get_assessment_rows_full_fields.json | 168 ++++ .../expected/review__get_citations_clean.json | 181 ++++ ...w__get_citations_failed_and_unchecked.json | 197 +++++ .../review__get_citations_issue_full.json | 203 +++++ ...view__get_citations_issue_issues_view.json | 93 ++ ..._get_citations_limit_exactly_at_limit.json | 181 ++++ .../review__get_citations_limit_reached.json | 155 ++++ ..._get_citations_no_claim_but_citations.json | 150 ++++ ...w__get_citations_no_claim_no_citation.json | 66 ++ .../review__get_citations_not_asked.json | 98 +++ .../expected/review__get_citations_only.json | 157 ++++ ...eview__get_citations_partly_supported.json | 181 ++++ .../review__get_citations_running.json | 165 ++++ ...w__get_citations_skipped_switched_off.json | 98 +++ ...view__get_citations_skipped_url_input.json | 66 ++ .../review__get_claim_limit_not_reached.json | 128 +++ ...review__get_claim_limit_reached_exact.json | 128 +++ .../review__get_completed_assess_only.json | 154 ++++ .../expected/review__get_completed_clean.json | 128 +++ ...ew__get_completed_issues_all_verified.json | 259 ++++++ .../review__get_completed_issues_full.json | 310 +++++++ ...iew__get_completed_issues_view_issues.json | 133 +++ .../review__get_completed_positions.json | 140 +++ ...review__get_completed_recovered_claim.json | 98 +++ ...ew__get_completed_with_untraced_claim.json | 140 +++ .../review__get_during_quick_check.json | 86 ++ ...w__get_failed_every_assessment_failed.json | 155 ++++ .../expected/review__get_failed_no_claim.json | 66 ++ .../review__get_failed_no_claim_untraced.json | 66 ++ .../expected/review__get_more_claims.json | 153 ++++ ...dified_at_crosses_midnight_by_minutes.json | 149 ++++ ...tion_modified_at_same_day_hours_apart.json | 149 ++++ .../expected/review__get_policy_resolved.json | 96 +++ .../parity/expected/review__get_queued.json | 60 ++ .../expected/review__get_suggested_edits.json | 267 ++++++ ...view__get_suggested_edits_issues_view.json | 128 +++ .../expected/review__get_url_review.json | 104 +++ .../expected/review__get_verifying.json | 177 ++++ ...review__idempotency_body_mismatch_422.json | 29 + .../review__idempotency_conflict_409.json | 21 + ...mpotency_conflict_409_existing_review.json | 21 + .../review__idempotent_replay_202.json | 7 + .../parity/expected/review__receipt_202.json | 7 + ...review__receipt_202_empty_webhook_url.json | 7 + .../review__receipt_202_webhook_url.json | 7 + .../expected/review__replay_later_202.json | 7 + .../parity/expected/verify__batch_202.json | 16 + .../expected/verify__batch_capacity_503.json | 22 + .../expected/verify__batch_empty_422.json | 22 + ...erify__batch_idempotency_conflict_409.json | 21 + .../verify__batch_item_missing_claim_422.json | 34 + .../verify__batch_no_credits_402.json | 28 + .../expected/verify__batch_partial_202.json | 13 + .../expected/verify__batch_too_many_422.json | 22 + .../expected/verify__blank_claim_422.json | 22 + .../parity/expected/verify__capacity_503.json | 22 + ...verify__idempotency_body_mismatch_422.json | 22 + .../verify__idempotency_conflict_409.json | 21 + .../verify__idempotency_key_replay.json | 9 + .../expected/verify__implicit_dedup_200.json | 8 + .../verify__implicit_repeat_replay.json | 9 + .../expected/verify__invalid_depth_422.json | 35 + .../verify__invalid_language_422.json | 22 + .../parity/expected/verify__list_200.json | 40 + ...dified_at_crosses_midnight_by_minutes.json | 25 + ..._200_modified_at_same_day_hours_apart.json | 25 + .../expected/verify__list_clamped_200.json | 9 + .../expected/verify__list_empty_200.json | 9 + .../expected/verify__list_page2_200.json | 25 + .../expected/verify__misnamed_field_422.json | 32 + .../expected/verify__missing_claim_422.json | 32 + .../expected/verify__no_credits_402.json | 28 + .../expected/verify__replay_later_202.json | 9 + .../parity/expected/verify__select_202.json | 16 + .../expected/verify__select_empty_422.json | 22 + .../verify__select_invalid_selection_422.json | 22 + ...rify__select_no_selection_pending_409.json | 21 + .../expected/verify__select_too_many_422.json | 22 + .../verify__select_unknown_task_404.json | 21 + ...erify__status_cancelled_after_a_while.json | 51 ++ .../expected/verify__status_completed.json | 203 +++++ ...erify__status_completed_after_a_while.json | 203 +++++ ...dified_at_crosses_midnight_by_minutes.json | 203 +++++ ...hile_modified_at_same_day_hours_apart.json | 203 +++++ ...dified_at_crosses_midnight_by_minutes.json | 203 +++++ ...eted_modified_at_same_day_hours_apart.json | 203 +++++ .../expected/verify__status_failed.json | 51 ++ .../verify__status_failed_after_a_while.json | 51 ++ ...__status_failed_after_a_while_framing.json | 51 ++ .../verify__status_failed_retryable.json | 51 ++ .../expected/verify__status_needs_input.json | 90 ++ .../expected/verify__status_not_a_claim.json | 51 ++ ...ify__status_not_a_claim_after_a_while.json | 51 ++ .../verify__status_not_yours_404.json | 21 + .../verify__status_older_run_completed.json | 203 +++++ ...rify__status_older_run_failed_crashed.json | 51 ++ ...lder_run_failed_insufficient_evidence.json | 51 ++ .../verify__status_older_run_in_progress.json | 51 ++ .../expected/verify__status_processing.json | 51 ++ ...rify__status_processing_after_a_while.json | 51 ++ .../expected/verify__status_task_stuck.json | 51 ++ .../expected/verify__status_unknown_404.json | 21 + .../parity/expected/verify__submit_202.json | 9 + .../expected/verify__submit_202_options.json | 9 + .../verify__submit_202_text_alias.json | 9 + .../expected/verify__unauthenticated_401.json | 21 + .../expected/verify__verification_200.json | 92 ++ .../verify__verification_200_covered.json | 90 ++ ...dified_at_crosses_midnight_by_minutes.json | 92 ++ ...fy__verification_200_modified_at_null.json | 92 ++ ..._200_modified_at_same_day_hours_apart.json | 92 ++ ...ify__verification_200_modified_at_set.json | 92 ++ ...ation_200_result_json_conclusion_only.json | 53 ++ ...y__verification_200_result_json_empty.json | 53 ++ ...ion_200_result_json_old_shape_strings.json | 69 ++ .../expected/verify__verification_404.json | 21 + .../verify__verification_by_task_id_200.json | 92 ++ .../verify__verification_failed_409.json | 26 + ..._failed_409_not_a_claim_after_a_while.json | 26 + .../verify__verification_not_ready_409.json | 24 + .../verify__verification_not_yours_404.json | 21 + ...verify__verification_public_anonymous.json | 80 ++ ...__verification_public_of_another_user.json | 92 ++ ...verify__verification_unknown_task_404.json | 21 + ...rification_zero_retention_expired_410.json | 22 + ...y__verification_zero_retention_served.json | 92 ++ .../webhook__certificate_timestamped.json | 23 + ..._certificate_timestamped_review_child.json | 23 + .../webhook__citecheck_completed.json | 135 +++ .../expected/webhook__citecheck_failed.json | 48 ++ .../expected/webhook__review_completed.json | 157 ++++ ...ook__review_completed_key_default_url.json | 106 +++ ...k__review_completed_oversized_rebuilt.json | 15 + .../expected/webhook__review_failed.json | 74 ++ .../webhook__verification_completed.json | 103 +++ ...hook__verification_completed_in_batch.json | 103 +++ ...dified_at_crosses_midnight_by_minutes.json | 103 +++ ...eted_modified_at_same_day_hours_apart.json | 103 +++ ...fication_failed_insufficient_evidence.json | 15 + ...hook__verification_failed_not_a_claim.json | 15 + ...ification_failed_upstream_unavailable.json | 15 + ..._verification_needs_input_multi_claim.json | 27 + .../parity/legacy/account__library_empty.json | 14 + ...dified_at_crosses_midnight_by_minutes.json | 30 + ...item_modified_at_same_day_hours_apart.json | 30 + .../parity/legacy/account__library_items.json | 60 ++ .../legacy/account__library_no_match.json | 14 + .../account__library_page_past_end.json | 14 + ...ccount__library_paginated_second_page.json | 45 + .../account__me_usage_extra_credits.json | 66 ++ .../legacy/account__me_usage_extra_only.json | 66 ++ .../account__me_usage_extract_calls.json | 66 ++ .../parity/legacy/account__me_usage_free.json | 66 ++ .../account__me_usage_free_partly_spent.json | 66 ++ .../parity/legacy/account__me_usage_plus.json | 66 ++ .../parity/legacy/account__me_usage_pro.json | 66 ++ .../legacy/account__me_usage_pro_extra.json | 66 ++ .../account__me_usage_stale_quota_row.json | 66 ++ .../parity/legacy/assess__401_no_key.json | 11 + .../parity/legacy/assess__402_no_credits.json | 19 + .../parity/legacy/assess__422_blank_item.json | 12 + .../parity/legacy/assess__422_blank_text.json | 11 + .../legacy/assess__422_input_conflict.json | 12 + .../legacy/assess__422_item_too_long.json | 12 + .../legacy/assess__422_no_input_field.json | 21 + .../legacy/assess__422_too_many_items.json | 12 + .../legacy/assess__422_wrong_type_claim.json | 21 + .../legacy/assess__422_wrong_type_claims.json | 21 + .../legacy/assess__502_framing_failed.json | 13 + .../parity/legacy/assess__503_capacity.json | 15 + ...ss__503_list_all_upstream_unavailable.json | 15 + .../assess__503_upstream_unavailable.json | 15 + .../parity/legacy/assess__deep_tier_hit.json | 28 + .../assess__idempotency_body_mismatch.json | 11 + .../legacy/assess__idempotency_first.json | 28 + .../legacy/assess__idempotency_replay.json | 28 + .../legacy/assess__list_all_error_rows.json | 42 + .../legacy/assess__list_compound_item.json | 45 + .../legacy/assess__list_mixed_rows.json | 70 ++ .../parity/legacy/assess__memo_hit.json | 28 + .../legacy/assess__replay_later_200.json | 28 + .../legacy/assess__single_no_claim.json | 15 + .../legacy/assess__single_one_claim.json | 28 + ...ss__single_text_over_wave_more_claims.json | 297 +++++++ .../assess__single_text_several_claims.json | 56 ++ .../legacy/assess__suggest_rewrite.json | 42 + .../legacy/citecheck__402_no_credits.json | 19 + .../legacy/citecheck__422_empty_body.json | 21 + .../legacy/citecheck__422_invalid_doi.json | 24 + .../citecheck__422_max_citations_over_20.json | 22 + ...techeck__422_max_citations_with_pairs.json | 21 + .../citecheck__422_pair_bad_url_scheme.json | 24 + .../legacy/citecheck__422_pair_no_source.json | 23 + .../citecheck__422_pair_url_and_doi.json | 23 + .../legacy/citecheck__422_pairs_empty.json | 22 + .../legacy/citecheck__422_pairs_over_20.json | 22 + ...citecheck__422_quote_not_in_statement.json | 23 + .../legacy/citecheck__422_text_and_pairs.json | 21 + .../legacy/citecheck__422_unknown_field.json | 24 + .../legacy/citecheck__422_url_input.json | 21 + .../citecheck__429_citecheck_in_flight.json | 14 + .../legacy/citecheck__503_capacity.json | 15 + .../citecheck__503_citations_unavailable.json | 15 + .../legacy/citecheck__get_404_not_found.json | 12 + .../legacy/citecheck__get_404_review_id.json | 12 + .../legacy/citecheck__get_410_purged.json | 13 + .../legacy/citecheck__get_checking.json | 103 +++ .../citecheck__get_completed_clean.json | 111 +++ .../citecheck__get_completed_doi_pair.json | 76 ++ .../citecheck__get_completed_incomplete.json | 127 +++ ...citecheck__get_completed_issues_found.json | 141 +++ ...check__get_completed_partly_supported.json | 111 +++ .../citecheck__get_completed_unchecked.json | 111 +++ .../citecheck__get_failed_no_citations.json | 42 + ...heck__get_failed_upstream_unavailable.json | 148 ++++ .../parity/legacy/citecheck__get_queued.json | 72 ++ ...heck__get_text_limit_exactly_at_limit.json | 119 +++ .../citecheck__get_text_limit_reached.json | 93 ++ ...echeck__idempotency_body_mismatch_422.json | 20 + .../citecheck__idempotency_conflict_409.json | 13 + .../citecheck__idempotent_replay_202.json | 14 + .../parity/legacy/citecheck__receipt_202.json | 14 + ...echeck__receipt_202_empty_webhook_url.json | 14 + .../legacy/errors__ask_bad_language.json | 11 + .../legacy/errors__ask_empty_message.json | 11 + .../parity/legacy/errors__ask_failed.json | 13 + .../errors__ask_idempotency_key_reused.json | 11 + .../legacy/errors__ask_message_too_long.json | 11 + .../legacy/errors__ask_not_completed.json | 11 + .../legacy/errors__ask_reset_not_found.json | 11 + .../legacy/errors__ask_send_not_found.json | 11 + .../parity/legacy/errors__auth_bad_key.json | 11 + .../legacy/errors__auth_basic_scheme.json | 11 + .../errors__auth_insufficient_scope.json | 14 + .../legacy/errors__auth_missing_bearer.json | 11 + .../errors__auth_missing_bearer_post.json | 11 + ...rrors__auth_private_claim_ask_history.json | 13 + .../errors__auth_private_claim_ask_send.json | 13 + .../errors__auth_unrecognised_prefix.json | 11 + ...rrors__http_error_library_bad_curated.json | 11 + ...rrors__http_error_library_bad_verdict.json | 11 + .../parity/legacy/errors__not_found_ask.json | 11 + .../legacy/errors__not_found_route.json | 11 + .../errors__not_found_verification.json | 11 + .../errors__not_found_verify_status.json | 11 + .../legacy/errors__payment_required_ask.json | 19 + .../errors__payment_required_assess.json | 19 + .../errors__payment_required_verify.json | 19 + ...__payment_required_verify_batch_short.json | 20 + .../legacy/errors__rate_limited_extract.json | 17 + .../errors__service_unavailable_ask.json | 15 + .../errors__service_unavailable_capacity.json | 15 + .../errors__validation_malformed_json.json | 11 + ...errors__validation_missing_field_hint.json | 27 + ...errors__validation_unknown_field_only.json | 26 + ...__validation_unknown_field_wrong_type.json | 25 + .../legacy/errors__validation_wrong_type.json | 21 + .../parity/legacy/extract__401_no_key.json | 11 + .../legacy/extract__422_blank_text.json | 11 + .../legacy/extract__422_focus_too_long.json | 11 + .../legacy/extract__422_missing_text.json | 21 + .../legacy/extract__429_daily_limit.json | 17 + .../extract__502_extraction_failed.json | 13 + .../extract__503_upstream_unavailable.json | 15 + .../parity/legacy/extract__focus_ready.json | 28 + ...xtract__idempotency_body_mismatch_422.json | 11 + .../legacy/extract__idempotency_replay.json | 31 + .../legacy/extract__locate_all_dropped.json | 28 + .../parity/legacy/extract__locate_failed.json | 31 + .../extract__locate_leading_whitespace.json | 39 + .../legacy/extract__locate_some_dropped.json | 52 ++ .../parity/legacy/extract__locate_true.json | 52 ++ .../legacy/extract__no_match_with_focus.json | 19 + .../parity/legacy/extract__not_a_claim.json | 19 + .../extract__not_a_claim_beside_claims.json | 28 + .../legacy/extract__ready_one_claim.json | 28 + .../legacy/extract__ready_several_claims.json | 31 + .../legacy/extract__replay_later_200.json | 31 + .../extract__url_fetch_502_unreadable.json | 13 + .../extract__url_fetch_503_upstream.json | 15 + .../legacy/extract__url_input_located.json | 39 + .../legacy/review__401_no_credentials.json | 11 + .../parity/legacy/review__402_no_credits.json | 19 + .../review__402_no_credits_exhausted.json | 19 + .../parity/legacy/review__422_blank_text.json | 21 + .../review__422_citations_options_object.json | 22 + .../legacy/review__422_depth_unknown.json | 23 + .../review__422_invalid_confidence_band.json | 23 + .../review__422_invalid_verdict_label.json | 23 + .../review__422_max_assessments_negative.json | 23 + .../review__422_max_citations_negative.json | 23 + .../review__422_max_citations_too_big.json | 23 + ...review__422_max_verifications_too_big.json | 23 + .../legacy/review__422_text_missing.json | 22 + .../legacy/review__422_text_wrong_type.json | 22 + .../review__422_unknown_escalate_field.json | 23 + .../review__422_unknown_top_level_field.json | 22 + .../review__422_unsupported_language.json | 21 + .../legacy/review__422_visibility_public.json | 22 + .../review__429_extract_daily_limit.json | 17 + .../legacy/review__429_review_in_flight.json | 14 + .../parity/legacy/review__503_capacity.json | 15 + .../review__503_citations_capacity.json | 15 + .../review__get_403_insufficient_scope.json | 14 + .../legacy/review__get_404_not_found.json | 12 + .../legacy/review__get_404_other_account.json | 12 + .../parity/legacy/review__get_410_purged.json | 13 + .../legacy/review__get_422_unknown_view.json | 21 + .../parity/legacy/review__get_assessing.json | 110 +++ ...view__get_assessment_rows_full_fields.json | 170 ++++ .../legacy/review__get_citations_clean.json | 183 ++++ ...w__get_citations_failed_and_unchecked.json | 199 +++++ .../review__get_citations_issue_full.json | 205 +++++ ...view__get_citations_issue_issues_view.json | 93 ++ ..._get_citations_limit_exactly_at_limit.json | 183 ++++ .../review__get_citations_limit_reached.json | 157 ++++ ..._get_citations_no_claim_but_citations.json | 152 ++++ ...w__get_citations_no_claim_no_citation.json | 68 ++ .../review__get_citations_not_asked.json | 100 +++ .../legacy/review__get_citations_only.json | 159 ++++ ...eview__get_citations_partly_supported.json | 183 ++++ .../legacy/review__get_citations_running.json | 167 ++++ ...w__get_citations_skipped_switched_off.json | 100 +++ ...view__get_citations_skipped_url_input.json | 68 ++ .../review__get_claim_limit_not_reached.json | 130 +++ ...review__get_claim_limit_reached_exact.json | 130 +++ .../review__get_completed_assess_only.json | 156 ++++ .../legacy/review__get_completed_clean.json | 130 +++ ...ew__get_completed_issues_all_verified.json | 261 ++++++ .../review__get_completed_issues_full.json | 312 +++++++ ...iew__get_completed_issues_view_issues.json | 133 +++ .../review__get_completed_positions.json | 142 +++ ...review__get_completed_recovered_claim.json | 100 +++ ...ew__get_completed_with_untraced_claim.json | 142 +++ .../review__get_during_quick_check.json | 88 ++ ...w__get_failed_every_assessment_failed.json | 157 ++++ .../legacy/review__get_failed_no_claim.json | 68 ++ .../review__get_failed_no_claim_untraced.json | 68 ++ .../legacy/review__get_more_claims.json | 155 ++++ ...dified_at_crosses_midnight_by_minutes.json | 151 ++++ ...tion_modified_at_same_day_hours_apart.json | 151 ++++ .../legacy/review__get_policy_resolved.json | 98 +++ .../parity/legacy/review__get_queued.json | 62 ++ .../legacy/review__get_suggested_edits.json | 269 ++++++ ...view__get_suggested_edits_issues_view.json | 128 +++ .../parity/legacy/review__get_url_review.json | 106 +++ .../parity/legacy/review__get_verifying.json | 179 ++++ ...review__idempotency_body_mismatch_422.json | 20 + .../review__idempotency_conflict_409.json | 13 + ...mpotency_conflict_409_existing_review.json | 13 + .../legacy/review__idempotent_replay_202.json | 14 + .../parity/legacy/review__receipt_202.json | 14 + ...review__receipt_202_empty_webhook_url.json | 14 + .../review__receipt_202_webhook_url.json | 14 + .../legacy/review__replay_later_202.json | 14 + .../parity/legacy/verify__batch_202.json | 21 + .../legacy/verify__batch_capacity_503.json | 15 + .../legacy/verify__batch_empty_422.json | 11 + ...erify__batch_idempotency_conflict_409.json | 12 + .../verify__batch_item_missing_claim_422.json | 23 + .../legacy/verify__batch_no_credits_402.json | 20 + .../legacy/verify__batch_partial_202.json | 18 + .../legacy/verify__batch_too_many_422.json | 11 + .../legacy/verify__blank_claim_422.json | 11 + .../parity/legacy/verify__capacity_503.json | 15 + ...verify__idempotency_body_mismatch_422.json | 11 + .../verify__idempotency_conflict_409.json | 12 + .../verify__idempotency_key_replay.json | 13 + .../legacy/verify__implicit_dedup_200.json | 12 + .../verify__implicit_repeat_replay.json | 13 + .../legacy/verify__invalid_depth_422.json | 24 + .../legacy/verify__invalid_language_422.json | 11 + .../parity/legacy/verify__list_200.json | 45 + ...dified_at_crosses_midnight_by_minutes.json | 30 + ..._200_modified_at_same_day_hours_apart.json | 30 + .../legacy/verify__list_clamped_200.json | 14 + .../parity/legacy/verify__list_empty_200.json | 14 + .../parity/legacy/verify__list_page2_200.json | 30 + .../legacy/verify__misnamed_field_422.json | 26 + .../legacy/verify__missing_claim_422.json | 21 + .../parity/legacy/verify__no_credits_402.json | 19 + .../legacy/verify__replay_later_202.json | 13 + .../parity/legacy/verify__select_202.json | 21 + .../legacy/verify__select_empty_422.json | 11 + .../verify__select_invalid_selection_422.json | 13 + ...rify__select_no_selection_pending_409.json | 13 + .../legacy/verify__select_too_many_422.json | 11 + .../verify__select_unknown_task_404.json | 11 + ...erify__status_cancelled_after_a_while.json | 17 + .../legacy/verify__status_completed.json | 99 +++ ...erify__status_completed_after_a_while.json | 99 +++ ...dified_at_crosses_midnight_by_minutes.json | 99 +++ ...hile_modified_at_same_day_hours_apart.json | 99 +++ ...dified_at_crosses_midnight_by_minutes.json | 99 +++ ...eted_modified_at_same_day_hours_apart.json | 99 +++ .../parity/legacy/verify__status_failed.json | 17 + .../verify__status_failed_after_a_while.json | 17 + ...__status_failed_after_a_while_framing.json | 17 + .../verify__status_failed_retryable.json | 17 + .../legacy/verify__status_needs_input.json | 24 + .../legacy/verify__status_not_a_claim.json | 18 + ...ify__status_not_a_claim_after_a_while.json | 17 + .../legacy/verify__status_not_yours_404.json | 11 + .../verify__status_older_run_completed.json | 99 +++ ...rify__status_older_run_failed_crashed.json | 17 + ...lder_run_failed_insufficient_evidence.json | 17 + .../verify__status_older_run_in_progress.json | 19 + .../legacy/verify__status_processing.json | 19 + ...rify__status_processing_after_a_while.json | 19 + .../legacy/verify__status_task_stuck.json | 17 + .../legacy/verify__status_unknown_404.json | 11 + .../parity/legacy/verify__submit_202.json | 13 + .../legacy/verify__submit_202_options.json | 13 + .../legacy/verify__submit_202_text_alias.json | 13 + .../legacy/verify__unauthenticated_401.json | 11 + .../legacy/verify__verification_200.json | 95 +++ .../verify__verification_200_covered.json | 93 ++ ...dified_at_crosses_midnight_by_minutes.json | 95 +++ ...fy__verification_200_modified_at_null.json | 95 +++ ..._200_modified_at_same_day_hours_apart.json | 95 +++ ...ify__verification_200_modified_at_set.json | 95 +++ ...ation_200_result_json_conclusion_only.json | 56 ++ ...y__verification_200_result_json_empty.json | 56 ++ ...ion_200_result_json_old_shape_strings.json | 72 ++ .../legacy/verify__verification_404.json | 11 + .../verify__verification_by_task_id_200.json | 95 +++ .../verify__verification_failed_409.json | 19 + ..._failed_409_not_a_claim_after_a_while.json | 19 + .../verify__verification_not_ready_409.json | 15 + .../verify__verification_not_yours_404.json | 11 + ...verify__verification_public_anonymous.json | 83 ++ ...__verification_public_of_another_user.json | 95 +++ ...verify__verification_unknown_task_404.json | 12 + ...rification_zero_retention_expired_410.json | 13 + ...y__verification_zero_retention_served.json | 95 +++ .../webhook__certificate_timestamped.json | 27 + ..._certificate_timestamped_review_child.json | 28 + .../legacy/webhook__citecheck_completed.json | 134 +++ .../legacy/webhook__citecheck_failed.json | 47 + .../legacy/webhook__review_completed.json | 156 ++++ ...ook__review_completed_key_default_url.json | 105 +++ ...k__review_completed_oversized_rebuilt.json | 11 + .../parity/legacy/webhook__review_failed.json | 73 ++ .../webhook__verification_completed.json | 107 +++ ...hook__verification_completed_in_batch.json | 107 +++ ...dified_at_crosses_midnight_by_minutes.json | 107 +++ ...eted_modified_at_same_day_hours_apart.json | 107 +++ ...fication_failed_insufficient_evidence.json | 19 + ...hook__verification_failed_not_a_claim.json | 19 + ...ification_failed_upstream_unavailable.json | 19 + ..._verification_needs_input_multi_claim.json | 30 + tests/fixtures/parity/requests.json | 68 ++ tests/parity_generate.py | 37 + tests/parity_observe.py | 233 +++++ tests/parity_requests.py | 99 +++ tests/test_cli.py | 2 +- tests/test_input_aliases.py | 3 +- tests/test_parity.py | 207 +++++ tests/test_read_both_shapes.py | 369 ++++++++ tests/test_usage_models.py | 12 +- 946 files changed, 48372 insertions(+), 312 deletions(-) create mode 100644 tests/fixtures/parity/canonical/account__library_empty.json create mode 100644 tests/fixtures/parity/canonical/account__library_item_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/canonical/account__library_item_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/canonical/account__library_items.json create mode 100644 tests/fixtures/parity/canonical/account__library_no_match.json create mode 100644 tests/fixtures/parity/canonical/account__library_page_past_end.json create mode 100644 tests/fixtures/parity/canonical/account__library_paginated_second_page.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_extra_credits.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_extra_only.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_extract_calls.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_free.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_free_partly_spent.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_plus.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_pro.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_pro_extra.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_stale_quota_row.json create mode 100644 tests/fixtures/parity/canonical/assess__401_no_key.json create mode 100644 tests/fixtures/parity/canonical/assess__402_no_credits.json create mode 100644 tests/fixtures/parity/canonical/assess__422_blank_item.json create mode 100644 tests/fixtures/parity/canonical/assess__422_blank_text.json create mode 100644 tests/fixtures/parity/canonical/assess__422_input_conflict.json create mode 100644 tests/fixtures/parity/canonical/assess__422_item_too_long.json create mode 100644 tests/fixtures/parity/canonical/assess__422_no_input_field.json create mode 100644 tests/fixtures/parity/canonical/assess__422_too_many_items.json create mode 100644 tests/fixtures/parity/canonical/assess__422_wrong_type_claim.json create mode 100644 tests/fixtures/parity/canonical/assess__422_wrong_type_claims.json create mode 100644 tests/fixtures/parity/canonical/assess__502_framing_failed.json create mode 100644 tests/fixtures/parity/canonical/assess__503_capacity.json create mode 100644 tests/fixtures/parity/canonical/assess__503_list_all_upstream_unavailable.json create mode 100644 tests/fixtures/parity/canonical/assess__503_upstream_unavailable.json create mode 100644 tests/fixtures/parity/canonical/assess__deep_tier_hit.json create mode 100644 tests/fixtures/parity/canonical/assess__idempotency_body_mismatch.json create mode 100644 tests/fixtures/parity/canonical/assess__idempotency_first.json create mode 100644 tests/fixtures/parity/canonical/assess__idempotency_replay.json create mode 100644 tests/fixtures/parity/canonical/assess__list_all_error_rows.json create mode 100644 tests/fixtures/parity/canonical/assess__list_compound_item.json create mode 100644 tests/fixtures/parity/canonical/assess__list_mixed_rows.json create mode 100644 tests/fixtures/parity/canonical/assess__memo_hit.json create mode 100644 tests/fixtures/parity/canonical/assess__replay_later_200.json create mode 100644 tests/fixtures/parity/canonical/assess__single_no_claim.json create mode 100644 tests/fixtures/parity/canonical/assess__single_one_claim.json create mode 100644 tests/fixtures/parity/canonical/assess__single_text_over_wave_more_claims.json create mode 100644 tests/fixtures/parity/canonical/assess__single_text_several_claims.json create mode 100644 tests/fixtures/parity/canonical/assess__suggest_rewrite.json create mode 100644 tests/fixtures/parity/canonical/citecheck__402_no_credits.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_empty_body.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_invalid_doi.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_max_citations_over_20.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_max_citations_with_pairs.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_pair_bad_url_scheme.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_pair_no_source.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_pair_url_and_doi.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_pairs_empty.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_pairs_over_20.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_quote_not_in_statement.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_text_and_pairs.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_unknown_field.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_url_input.json create mode 100644 tests/fixtures/parity/canonical/citecheck__429_citecheck_in_flight.json create mode 100644 tests/fixtures/parity/canonical/citecheck__503_capacity.json create mode 100644 tests/fixtures/parity/canonical/citecheck__503_citations_unavailable.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_404_not_found.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_404_review_id.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_410_purged.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_checking.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_completed_clean.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_completed_doi_pair.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_completed_incomplete.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_completed_issues_found.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_completed_partly_supported.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_completed_unchecked.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_failed_no_citations.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_failed_upstream_unavailable.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_queued.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_text_limit_exactly_at_limit.json create mode 100644 tests/fixtures/parity/canonical/citecheck__get_text_limit_reached.json create mode 100644 tests/fixtures/parity/canonical/citecheck__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/canonical/citecheck__idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/canonical/citecheck__idempotent_replay_202.json create mode 100644 tests/fixtures/parity/canonical/citecheck__receipt_202.json create mode 100644 tests/fixtures/parity/canonical/citecheck__receipt_202_empty_webhook_url.json create mode 100644 tests/fixtures/parity/canonical/errors__ask_bad_language.json create mode 100644 tests/fixtures/parity/canonical/errors__ask_empty_message.json create mode 100644 tests/fixtures/parity/canonical/errors__ask_failed.json create mode 100644 tests/fixtures/parity/canonical/errors__ask_idempotency_key_reused.json create mode 100644 tests/fixtures/parity/canonical/errors__ask_message_too_long.json create mode 100644 tests/fixtures/parity/canonical/errors__ask_not_completed.json create mode 100644 tests/fixtures/parity/canonical/errors__ask_reset_not_found.json create mode 100644 tests/fixtures/parity/canonical/errors__ask_send_not_found.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_bad_key.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_basic_scheme.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_insufficient_scope.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_missing_bearer.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_missing_bearer_post.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_private_claim_ask_history.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_private_claim_ask_send.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_unrecognised_prefix.json create mode 100644 tests/fixtures/parity/canonical/errors__http_error_library_bad_curated.json create mode 100644 tests/fixtures/parity/canonical/errors__http_error_library_bad_verdict.json create mode 100644 tests/fixtures/parity/canonical/errors__not_found_ask.json create mode 100644 tests/fixtures/parity/canonical/errors__not_found_route.json create mode 100644 tests/fixtures/parity/canonical/errors__not_found_verification.json create mode 100644 tests/fixtures/parity/canonical/errors__not_found_verify_status.json create mode 100644 tests/fixtures/parity/canonical/errors__payment_required_ask.json create mode 100644 tests/fixtures/parity/canonical/errors__payment_required_assess.json create mode 100644 tests/fixtures/parity/canonical/errors__payment_required_verify.json create mode 100644 tests/fixtures/parity/canonical/errors__payment_required_verify_batch_short.json create mode 100644 tests/fixtures/parity/canonical/errors__rate_limited_extract.json create mode 100644 tests/fixtures/parity/canonical/errors__service_unavailable_ask.json create mode 100644 tests/fixtures/parity/canonical/errors__service_unavailable_capacity.json create mode 100644 tests/fixtures/parity/canonical/errors__validation_malformed_json.json create mode 100644 tests/fixtures/parity/canonical/errors__validation_missing_field_hint.json create mode 100644 tests/fixtures/parity/canonical/errors__validation_unknown_field_only.json create mode 100644 tests/fixtures/parity/canonical/errors__validation_unknown_field_wrong_type.json create mode 100644 tests/fixtures/parity/canonical/errors__validation_wrong_type.json create mode 100644 tests/fixtures/parity/canonical/extract__401_no_key.json create mode 100644 tests/fixtures/parity/canonical/extract__422_blank_text.json create mode 100644 tests/fixtures/parity/canonical/extract__422_focus_too_long.json create mode 100644 tests/fixtures/parity/canonical/extract__422_missing_text.json create mode 100644 tests/fixtures/parity/canonical/extract__429_daily_limit.json create mode 100644 tests/fixtures/parity/canonical/extract__502_extraction_failed.json create mode 100644 tests/fixtures/parity/canonical/extract__503_upstream_unavailable.json create mode 100644 tests/fixtures/parity/canonical/extract__focus_ready.json create mode 100644 tests/fixtures/parity/canonical/extract__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/canonical/extract__idempotency_replay.json create mode 100644 tests/fixtures/parity/canonical/extract__locate_all_dropped.json create mode 100644 tests/fixtures/parity/canonical/extract__locate_failed.json create mode 100644 tests/fixtures/parity/canonical/extract__locate_leading_whitespace.json create mode 100644 tests/fixtures/parity/canonical/extract__locate_some_dropped.json create mode 100644 tests/fixtures/parity/canonical/extract__locate_true.json create mode 100644 tests/fixtures/parity/canonical/extract__no_match_with_focus.json create mode 100644 tests/fixtures/parity/canonical/extract__not_a_claim.json create mode 100644 tests/fixtures/parity/canonical/extract__not_a_claim_beside_claims.json create mode 100644 tests/fixtures/parity/canonical/extract__ready_one_claim.json create mode 100644 tests/fixtures/parity/canonical/extract__ready_several_claims.json create mode 100644 tests/fixtures/parity/canonical/extract__replay_later_200.json create mode 100644 tests/fixtures/parity/canonical/extract__url_fetch_502_unreadable.json create mode 100644 tests/fixtures/parity/canonical/extract__url_fetch_503_upstream.json create mode 100644 tests/fixtures/parity/canonical/extract__url_input_located.json create mode 100644 tests/fixtures/parity/canonical/review__401_no_credentials.json create mode 100644 tests/fixtures/parity/canonical/review__402_no_credits.json create mode 100644 tests/fixtures/parity/canonical/review__402_no_credits_exhausted.json create mode 100644 tests/fixtures/parity/canonical/review__422_blank_text.json create mode 100644 tests/fixtures/parity/canonical/review__422_citations_options_object.json create mode 100644 tests/fixtures/parity/canonical/review__422_depth_unknown.json create mode 100644 tests/fixtures/parity/canonical/review__422_invalid_confidence_band.json create mode 100644 tests/fixtures/parity/canonical/review__422_invalid_verdict_label.json create mode 100644 tests/fixtures/parity/canonical/review__422_max_assessments_negative.json create mode 100644 tests/fixtures/parity/canonical/review__422_max_citations_negative.json create mode 100644 tests/fixtures/parity/canonical/review__422_max_citations_too_big.json create mode 100644 tests/fixtures/parity/canonical/review__422_max_verifications_too_big.json create mode 100644 tests/fixtures/parity/canonical/review__422_text_missing.json create mode 100644 tests/fixtures/parity/canonical/review__422_text_wrong_type.json create mode 100644 tests/fixtures/parity/canonical/review__422_unknown_escalate_field.json create mode 100644 tests/fixtures/parity/canonical/review__422_unknown_top_level_field.json create mode 100644 tests/fixtures/parity/canonical/review__422_unsupported_language.json create mode 100644 tests/fixtures/parity/canonical/review__422_visibility_public.json create mode 100644 tests/fixtures/parity/canonical/review__429_extract_daily_limit.json create mode 100644 tests/fixtures/parity/canonical/review__429_review_in_flight.json create mode 100644 tests/fixtures/parity/canonical/review__503_capacity.json create mode 100644 tests/fixtures/parity/canonical/review__503_citations_capacity.json create mode 100644 tests/fixtures/parity/canonical/review__get_403_insufficient_scope.json create mode 100644 tests/fixtures/parity/canonical/review__get_404_not_found.json create mode 100644 tests/fixtures/parity/canonical/review__get_404_other_account.json create mode 100644 tests/fixtures/parity/canonical/review__get_410_purged.json create mode 100644 tests/fixtures/parity/canonical/review__get_422_unknown_view.json create mode 100644 tests/fixtures/parity/canonical/review__get_assessing.json create mode 100644 tests/fixtures/parity/canonical/review__get_assessment_rows_full_fields.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_clean.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_failed_and_unchecked.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_issue_full.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_issue_issues_view.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_limit_exactly_at_limit.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_limit_reached.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_no_claim_but_citations.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_no_claim_no_citation.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_not_asked.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_only.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_partly_supported.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_running.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_skipped_switched_off.json create mode 100644 tests/fixtures/parity/canonical/review__get_citations_skipped_url_input.json create mode 100644 tests/fixtures/parity/canonical/review__get_claim_limit_not_reached.json create mode 100644 tests/fixtures/parity/canonical/review__get_claim_limit_reached_exact.json create mode 100644 tests/fixtures/parity/canonical/review__get_completed_assess_only.json create mode 100644 tests/fixtures/parity/canonical/review__get_completed_clean.json create mode 100644 tests/fixtures/parity/canonical/review__get_completed_issues_all_verified.json create mode 100644 tests/fixtures/parity/canonical/review__get_completed_issues_full.json create mode 100644 tests/fixtures/parity/canonical/review__get_completed_issues_view_issues.json create mode 100644 tests/fixtures/parity/canonical/review__get_completed_positions.json create mode 100644 tests/fixtures/parity/canonical/review__get_completed_recovered_claim.json create mode 100644 tests/fixtures/parity/canonical/review__get_completed_with_untraced_claim.json create mode 100644 tests/fixtures/parity/canonical/review__get_during_quick_check.json create mode 100644 tests/fixtures/parity/canonical/review__get_failed_every_assessment_failed.json create mode 100644 tests/fixtures/parity/canonical/review__get_failed_no_claim.json create mode 100644 tests/fixtures/parity/canonical/review__get_failed_no_claim_untraced.json create mode 100644 tests/fixtures/parity/canonical/review__get_more_claims.json create mode 100644 tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/canonical/review__get_policy_resolved.json create mode 100644 tests/fixtures/parity/canonical/review__get_queued.json create mode 100644 tests/fixtures/parity/canonical/review__get_suggested_edits.json create mode 100644 tests/fixtures/parity/canonical/review__get_suggested_edits_issues_view.json create mode 100644 tests/fixtures/parity/canonical/review__get_url_review.json create mode 100644 tests/fixtures/parity/canonical/review__get_verifying.json create mode 100644 tests/fixtures/parity/canonical/review__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/canonical/review__idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/canonical/review__idempotency_conflict_409_existing_review.json create mode 100644 tests/fixtures/parity/canonical/review__idempotent_replay_202.json create mode 100644 tests/fixtures/parity/canonical/review__receipt_202.json create mode 100644 tests/fixtures/parity/canonical/review__receipt_202_empty_webhook_url.json create mode 100644 tests/fixtures/parity/canonical/review__receipt_202_webhook_url.json create mode 100644 tests/fixtures/parity/canonical/review__replay_later_202.json create mode 100644 tests/fixtures/parity/canonical/verify__batch_202.json create mode 100644 tests/fixtures/parity/canonical/verify__batch_capacity_503.json create mode 100644 tests/fixtures/parity/canonical/verify__batch_empty_422.json create mode 100644 tests/fixtures/parity/canonical/verify__batch_idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/canonical/verify__batch_item_missing_claim_422.json create mode 100644 tests/fixtures/parity/canonical/verify__batch_no_credits_402.json create mode 100644 tests/fixtures/parity/canonical/verify__batch_partial_202.json create mode 100644 tests/fixtures/parity/canonical/verify__batch_too_many_422.json create mode 100644 tests/fixtures/parity/canonical/verify__blank_claim_422.json create mode 100644 tests/fixtures/parity/canonical/verify__capacity_503.json create mode 100644 tests/fixtures/parity/canonical/verify__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/canonical/verify__idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/canonical/verify__idempotency_key_replay.json create mode 100644 tests/fixtures/parity/canonical/verify__implicit_dedup_200.json create mode 100644 tests/fixtures/parity/canonical/verify__implicit_repeat_replay.json create mode 100644 tests/fixtures/parity/canonical/verify__invalid_depth_422.json create mode 100644 tests/fixtures/parity/canonical/verify__invalid_language_422.json create mode 100644 tests/fixtures/parity/canonical/verify__list_200.json create mode 100644 tests/fixtures/parity/canonical/verify__list_200_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/canonical/verify__list_200_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/canonical/verify__list_clamped_200.json create mode 100644 tests/fixtures/parity/canonical/verify__list_empty_200.json create mode 100644 tests/fixtures/parity/canonical/verify__list_page2_200.json create mode 100644 tests/fixtures/parity/canonical/verify__misnamed_field_422.json create mode 100644 tests/fixtures/parity/canonical/verify__missing_claim_422.json create mode 100644 tests/fixtures/parity/canonical/verify__no_credits_402.json create mode 100644 tests/fixtures/parity/canonical/verify__replay_later_202.json create mode 100644 tests/fixtures/parity/canonical/verify__select_202.json create mode 100644 tests/fixtures/parity/canonical/verify__select_empty_422.json create mode 100644 tests/fixtures/parity/canonical/verify__select_invalid_selection_422.json create mode 100644 tests/fixtures/parity/canonical/verify__select_no_selection_pending_409.json create mode 100644 tests/fixtures/parity/canonical/verify__select_too_many_422.json create mode 100644 tests/fixtures/parity/canonical/verify__select_unknown_task_404.json create mode 100644 tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json create mode 100644 tests/fixtures/parity/canonical/verify__status_completed.json create mode 100644 tests/fixtures/parity/canonical/verify__status_completed_after_a_while.json create mode 100644 tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/canonical/verify__status_completed_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/canonical/verify__status_completed_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/canonical/verify__status_failed.json create mode 100644 tests/fixtures/parity/canonical/verify__status_failed_after_a_while.json create mode 100644 tests/fixtures/parity/canonical/verify__status_failed_after_a_while_framing.json create mode 100644 tests/fixtures/parity/canonical/verify__status_failed_retryable.json create mode 100644 tests/fixtures/parity/canonical/verify__status_needs_input.json create mode 100644 tests/fixtures/parity/canonical/verify__status_not_a_claim.json create mode 100644 tests/fixtures/parity/canonical/verify__status_not_a_claim_after_a_while.json create mode 100644 tests/fixtures/parity/canonical/verify__status_not_yours_404.json create mode 100644 tests/fixtures/parity/canonical/verify__status_older_run_completed.json create mode 100644 tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json create mode 100644 tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json create mode 100644 tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json create mode 100644 tests/fixtures/parity/canonical/verify__status_processing.json create mode 100644 tests/fixtures/parity/canonical/verify__status_processing_after_a_while.json create mode 100644 tests/fixtures/parity/canonical/verify__status_task_stuck.json create mode 100644 tests/fixtures/parity/canonical/verify__status_unknown_404.json create mode 100644 tests/fixtures/parity/canonical/verify__submit_202.json create mode 100644 tests/fixtures/parity/canonical/verify__submit_202_options.json create mode 100644 tests/fixtures/parity/canonical/verify__submit_202_text_alias.json create mode 100644 tests/fixtures/parity/canonical/verify__unauthenticated_401.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_200.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_200_covered.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_200_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_200_modified_at_null.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_200_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_200_modified_at_set.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_200_result_json_conclusion_only.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_200_result_json_empty.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_200_result_json_old_shape_strings.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_404.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_by_task_id_200.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_failed_409.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_failed_409_not_a_claim_after_a_while.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_not_ready_409.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_not_yours_404.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_public_anonymous.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_public_of_another_user.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_unknown_task_404.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_zero_retention_expired_410.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_zero_retention_served.json create mode 100644 tests/fixtures/parity/canonical/webhook__certificate_timestamped.json create mode 100644 tests/fixtures/parity/canonical/webhook__certificate_timestamped_review_child.json create mode 100644 tests/fixtures/parity/canonical/webhook__citecheck_completed.json create mode 100644 tests/fixtures/parity/canonical/webhook__citecheck_failed.json create mode 100644 tests/fixtures/parity/canonical/webhook__review_completed.json create mode 100644 tests/fixtures/parity/canonical/webhook__review_completed_key_default_url.json create mode 100644 tests/fixtures/parity/canonical/webhook__review_completed_oversized_rebuilt.json create mode 100644 tests/fixtures/parity/canonical/webhook__review_failed.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_completed.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_completed_in_batch.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_failed_insufficient_evidence.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_failed_not_a_claim.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_failed_upstream_unavailable.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_needs_input_multi_claim.json create mode 100644 tests/fixtures/parity/expected/account__library_empty.json create mode 100644 tests/fixtures/parity/expected/account__library_item_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/expected/account__library_item_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/expected/account__library_items.json create mode 100644 tests/fixtures/parity/expected/account__library_no_match.json create mode 100644 tests/fixtures/parity/expected/account__library_page_past_end.json create mode 100644 tests/fixtures/parity/expected/account__library_paginated_second_page.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_extra_credits.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_extra_only.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_extract_calls.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_free.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_free_partly_spent.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_plus.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_pro.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_pro_extra.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_stale_quota_row.json create mode 100644 tests/fixtures/parity/expected/assess__401_no_key.json create mode 100644 tests/fixtures/parity/expected/assess__402_no_credits.json create mode 100644 tests/fixtures/parity/expected/assess__422_blank_item.json create mode 100644 tests/fixtures/parity/expected/assess__422_blank_text.json create mode 100644 tests/fixtures/parity/expected/assess__422_input_conflict.json create mode 100644 tests/fixtures/parity/expected/assess__422_item_too_long.json create mode 100644 tests/fixtures/parity/expected/assess__422_no_input_field.json create mode 100644 tests/fixtures/parity/expected/assess__422_too_many_items.json create mode 100644 tests/fixtures/parity/expected/assess__422_wrong_type_claim.json create mode 100644 tests/fixtures/parity/expected/assess__422_wrong_type_claims.json create mode 100644 tests/fixtures/parity/expected/assess__502_framing_failed.json create mode 100644 tests/fixtures/parity/expected/assess__503_capacity.json create mode 100644 tests/fixtures/parity/expected/assess__503_list_all_upstream_unavailable.json create mode 100644 tests/fixtures/parity/expected/assess__503_upstream_unavailable.json create mode 100644 tests/fixtures/parity/expected/assess__deep_tier_hit.json create mode 100644 tests/fixtures/parity/expected/assess__idempotency_body_mismatch.json create mode 100644 tests/fixtures/parity/expected/assess__idempotency_first.json create mode 100644 tests/fixtures/parity/expected/assess__idempotency_replay.json create mode 100644 tests/fixtures/parity/expected/assess__list_all_error_rows.json create mode 100644 tests/fixtures/parity/expected/assess__list_compound_item.json create mode 100644 tests/fixtures/parity/expected/assess__list_mixed_rows.json create mode 100644 tests/fixtures/parity/expected/assess__memo_hit.json create mode 100644 tests/fixtures/parity/expected/assess__replay_later_200.json create mode 100644 tests/fixtures/parity/expected/assess__single_no_claim.json create mode 100644 tests/fixtures/parity/expected/assess__single_one_claim.json create mode 100644 tests/fixtures/parity/expected/assess__single_text_over_wave_more_claims.json create mode 100644 tests/fixtures/parity/expected/assess__single_text_several_claims.json create mode 100644 tests/fixtures/parity/expected/assess__suggest_rewrite.json create mode 100644 tests/fixtures/parity/expected/citecheck__402_no_credits.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_empty_body.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_invalid_doi.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_max_citations_over_20.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_max_citations_with_pairs.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_pair_bad_url_scheme.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_pair_no_source.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_pair_url_and_doi.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_pairs_empty.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_pairs_over_20.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_quote_not_in_statement.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_text_and_pairs.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_unknown_field.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_url_input.json create mode 100644 tests/fixtures/parity/expected/citecheck__429_citecheck_in_flight.json create mode 100644 tests/fixtures/parity/expected/citecheck__503_capacity.json create mode 100644 tests/fixtures/parity/expected/citecheck__503_citations_unavailable.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_404_not_found.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_404_review_id.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_410_purged.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_checking.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_completed_clean.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_completed_doi_pair.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_completed_incomplete.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_completed_issues_found.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_completed_partly_supported.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_completed_unchecked.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_failed_no_citations.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_failed_upstream_unavailable.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_queued.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_text_limit_exactly_at_limit.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_text_limit_reached.json create mode 100644 tests/fixtures/parity/expected/citecheck__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/expected/citecheck__idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/expected/citecheck__idempotent_replay_202.json create mode 100644 tests/fixtures/parity/expected/citecheck__receipt_202.json create mode 100644 tests/fixtures/parity/expected/citecheck__receipt_202_empty_webhook_url.json create mode 100644 tests/fixtures/parity/expected/errors__ask_bad_language.json create mode 100644 tests/fixtures/parity/expected/errors__ask_empty_message.json create mode 100644 tests/fixtures/parity/expected/errors__ask_failed.json create mode 100644 tests/fixtures/parity/expected/errors__ask_idempotency_key_reused.json create mode 100644 tests/fixtures/parity/expected/errors__ask_message_too_long.json create mode 100644 tests/fixtures/parity/expected/errors__ask_not_completed.json create mode 100644 tests/fixtures/parity/expected/errors__ask_reset_not_found.json create mode 100644 tests/fixtures/parity/expected/errors__ask_send_not_found.json create mode 100644 tests/fixtures/parity/expected/errors__auth_bad_key.json create mode 100644 tests/fixtures/parity/expected/errors__auth_basic_scheme.json create mode 100644 tests/fixtures/parity/expected/errors__auth_insufficient_scope.json create mode 100644 tests/fixtures/parity/expected/errors__auth_missing_bearer.json create mode 100644 tests/fixtures/parity/expected/errors__auth_missing_bearer_post.json create mode 100644 tests/fixtures/parity/expected/errors__auth_private_claim_ask_history.json create mode 100644 tests/fixtures/parity/expected/errors__auth_private_claim_ask_send.json create mode 100644 tests/fixtures/parity/expected/errors__auth_unrecognised_prefix.json create mode 100644 tests/fixtures/parity/expected/errors__http_error_library_bad_curated.json create mode 100644 tests/fixtures/parity/expected/errors__http_error_library_bad_verdict.json create mode 100644 tests/fixtures/parity/expected/errors__not_found_ask.json create mode 100644 tests/fixtures/parity/expected/errors__not_found_route.json create mode 100644 tests/fixtures/parity/expected/errors__not_found_verification.json create mode 100644 tests/fixtures/parity/expected/errors__not_found_verify_status.json create mode 100644 tests/fixtures/parity/expected/errors__payment_required_ask.json create mode 100644 tests/fixtures/parity/expected/errors__payment_required_assess.json create mode 100644 tests/fixtures/parity/expected/errors__payment_required_verify.json create mode 100644 tests/fixtures/parity/expected/errors__payment_required_verify_batch_short.json create mode 100644 tests/fixtures/parity/expected/errors__rate_limited_extract.json create mode 100644 tests/fixtures/parity/expected/errors__service_unavailable_ask.json create mode 100644 tests/fixtures/parity/expected/errors__service_unavailable_capacity.json create mode 100644 tests/fixtures/parity/expected/errors__validation_malformed_json.json create mode 100644 tests/fixtures/parity/expected/errors__validation_missing_field_hint.json create mode 100644 tests/fixtures/parity/expected/errors__validation_unknown_field_only.json create mode 100644 tests/fixtures/parity/expected/errors__validation_unknown_field_wrong_type.json create mode 100644 tests/fixtures/parity/expected/errors__validation_wrong_type.json create mode 100644 tests/fixtures/parity/expected/extract__401_no_key.json create mode 100644 tests/fixtures/parity/expected/extract__422_blank_text.json create mode 100644 tests/fixtures/parity/expected/extract__422_focus_too_long.json create mode 100644 tests/fixtures/parity/expected/extract__422_missing_text.json create mode 100644 tests/fixtures/parity/expected/extract__429_daily_limit.json create mode 100644 tests/fixtures/parity/expected/extract__502_extraction_failed.json create mode 100644 tests/fixtures/parity/expected/extract__503_upstream_unavailable.json create mode 100644 tests/fixtures/parity/expected/extract__focus_ready.json create mode 100644 tests/fixtures/parity/expected/extract__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/expected/extract__idempotency_replay.json create mode 100644 tests/fixtures/parity/expected/extract__locate_all_dropped.json create mode 100644 tests/fixtures/parity/expected/extract__locate_failed.json create mode 100644 tests/fixtures/parity/expected/extract__locate_leading_whitespace.json create mode 100644 tests/fixtures/parity/expected/extract__locate_some_dropped.json create mode 100644 tests/fixtures/parity/expected/extract__locate_true.json create mode 100644 tests/fixtures/parity/expected/extract__no_match_with_focus.json create mode 100644 tests/fixtures/parity/expected/extract__not_a_claim.json create mode 100644 tests/fixtures/parity/expected/extract__not_a_claim_beside_claims.json create mode 100644 tests/fixtures/parity/expected/extract__ready_one_claim.json create mode 100644 tests/fixtures/parity/expected/extract__ready_several_claims.json create mode 100644 tests/fixtures/parity/expected/extract__replay_later_200.json create mode 100644 tests/fixtures/parity/expected/extract__url_fetch_502_unreadable.json create mode 100644 tests/fixtures/parity/expected/extract__url_fetch_503_upstream.json create mode 100644 tests/fixtures/parity/expected/extract__url_input_located.json create mode 100644 tests/fixtures/parity/expected/review__401_no_credentials.json create mode 100644 tests/fixtures/parity/expected/review__402_no_credits.json create mode 100644 tests/fixtures/parity/expected/review__402_no_credits_exhausted.json create mode 100644 tests/fixtures/parity/expected/review__422_blank_text.json create mode 100644 tests/fixtures/parity/expected/review__422_citations_options_object.json create mode 100644 tests/fixtures/parity/expected/review__422_depth_unknown.json create mode 100644 tests/fixtures/parity/expected/review__422_invalid_confidence_band.json create mode 100644 tests/fixtures/parity/expected/review__422_invalid_verdict_label.json create mode 100644 tests/fixtures/parity/expected/review__422_max_assessments_negative.json create mode 100644 tests/fixtures/parity/expected/review__422_max_citations_negative.json create mode 100644 tests/fixtures/parity/expected/review__422_max_citations_too_big.json create mode 100644 tests/fixtures/parity/expected/review__422_max_verifications_too_big.json create mode 100644 tests/fixtures/parity/expected/review__422_text_missing.json create mode 100644 tests/fixtures/parity/expected/review__422_text_wrong_type.json create mode 100644 tests/fixtures/parity/expected/review__422_unknown_escalate_field.json create mode 100644 tests/fixtures/parity/expected/review__422_unknown_top_level_field.json create mode 100644 tests/fixtures/parity/expected/review__422_unsupported_language.json create mode 100644 tests/fixtures/parity/expected/review__422_visibility_public.json create mode 100644 tests/fixtures/parity/expected/review__429_extract_daily_limit.json create mode 100644 tests/fixtures/parity/expected/review__429_review_in_flight.json create mode 100644 tests/fixtures/parity/expected/review__503_capacity.json create mode 100644 tests/fixtures/parity/expected/review__503_citations_capacity.json create mode 100644 tests/fixtures/parity/expected/review__get_403_insufficient_scope.json create mode 100644 tests/fixtures/parity/expected/review__get_404_not_found.json create mode 100644 tests/fixtures/parity/expected/review__get_404_other_account.json create mode 100644 tests/fixtures/parity/expected/review__get_410_purged.json create mode 100644 tests/fixtures/parity/expected/review__get_422_unknown_view.json create mode 100644 tests/fixtures/parity/expected/review__get_assessing.json create mode 100644 tests/fixtures/parity/expected/review__get_assessment_rows_full_fields.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_clean.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_failed_and_unchecked.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_issue_full.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_issue_issues_view.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_limit_exactly_at_limit.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_limit_reached.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_no_claim_but_citations.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_no_claim_no_citation.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_not_asked.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_only.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_partly_supported.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_running.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_skipped_switched_off.json create mode 100644 tests/fixtures/parity/expected/review__get_citations_skipped_url_input.json create mode 100644 tests/fixtures/parity/expected/review__get_claim_limit_not_reached.json create mode 100644 tests/fixtures/parity/expected/review__get_claim_limit_reached_exact.json create mode 100644 tests/fixtures/parity/expected/review__get_completed_assess_only.json create mode 100644 tests/fixtures/parity/expected/review__get_completed_clean.json create mode 100644 tests/fixtures/parity/expected/review__get_completed_issues_all_verified.json create mode 100644 tests/fixtures/parity/expected/review__get_completed_issues_full.json create mode 100644 tests/fixtures/parity/expected/review__get_completed_issues_view_issues.json create mode 100644 tests/fixtures/parity/expected/review__get_completed_positions.json create mode 100644 tests/fixtures/parity/expected/review__get_completed_recovered_claim.json create mode 100644 tests/fixtures/parity/expected/review__get_completed_with_untraced_claim.json create mode 100644 tests/fixtures/parity/expected/review__get_during_quick_check.json create mode 100644 tests/fixtures/parity/expected/review__get_failed_every_assessment_failed.json create mode 100644 tests/fixtures/parity/expected/review__get_failed_no_claim.json create mode 100644 tests/fixtures/parity/expected/review__get_failed_no_claim_untraced.json create mode 100644 tests/fixtures/parity/expected/review__get_more_claims.json create mode 100644 tests/fixtures/parity/expected/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/expected/review__get_nested_verification_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/expected/review__get_policy_resolved.json create mode 100644 tests/fixtures/parity/expected/review__get_queued.json create mode 100644 tests/fixtures/parity/expected/review__get_suggested_edits.json create mode 100644 tests/fixtures/parity/expected/review__get_suggested_edits_issues_view.json create mode 100644 tests/fixtures/parity/expected/review__get_url_review.json create mode 100644 tests/fixtures/parity/expected/review__get_verifying.json create mode 100644 tests/fixtures/parity/expected/review__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/expected/review__idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/expected/review__idempotency_conflict_409_existing_review.json create mode 100644 tests/fixtures/parity/expected/review__idempotent_replay_202.json create mode 100644 tests/fixtures/parity/expected/review__receipt_202.json create mode 100644 tests/fixtures/parity/expected/review__receipt_202_empty_webhook_url.json create mode 100644 tests/fixtures/parity/expected/review__receipt_202_webhook_url.json create mode 100644 tests/fixtures/parity/expected/review__replay_later_202.json create mode 100644 tests/fixtures/parity/expected/verify__batch_202.json create mode 100644 tests/fixtures/parity/expected/verify__batch_capacity_503.json create mode 100644 tests/fixtures/parity/expected/verify__batch_empty_422.json create mode 100644 tests/fixtures/parity/expected/verify__batch_idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/expected/verify__batch_item_missing_claim_422.json create mode 100644 tests/fixtures/parity/expected/verify__batch_no_credits_402.json create mode 100644 tests/fixtures/parity/expected/verify__batch_partial_202.json create mode 100644 tests/fixtures/parity/expected/verify__batch_too_many_422.json create mode 100644 tests/fixtures/parity/expected/verify__blank_claim_422.json create mode 100644 tests/fixtures/parity/expected/verify__capacity_503.json create mode 100644 tests/fixtures/parity/expected/verify__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/expected/verify__idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/expected/verify__idempotency_key_replay.json create mode 100644 tests/fixtures/parity/expected/verify__implicit_dedup_200.json create mode 100644 tests/fixtures/parity/expected/verify__implicit_repeat_replay.json create mode 100644 tests/fixtures/parity/expected/verify__invalid_depth_422.json create mode 100644 tests/fixtures/parity/expected/verify__invalid_language_422.json create mode 100644 tests/fixtures/parity/expected/verify__list_200.json create mode 100644 tests/fixtures/parity/expected/verify__list_200_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/expected/verify__list_200_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/expected/verify__list_clamped_200.json create mode 100644 tests/fixtures/parity/expected/verify__list_empty_200.json create mode 100644 tests/fixtures/parity/expected/verify__list_page2_200.json create mode 100644 tests/fixtures/parity/expected/verify__misnamed_field_422.json create mode 100644 tests/fixtures/parity/expected/verify__missing_claim_422.json create mode 100644 tests/fixtures/parity/expected/verify__no_credits_402.json create mode 100644 tests/fixtures/parity/expected/verify__replay_later_202.json create mode 100644 tests/fixtures/parity/expected/verify__select_202.json create mode 100644 tests/fixtures/parity/expected/verify__select_empty_422.json create mode 100644 tests/fixtures/parity/expected/verify__select_invalid_selection_422.json create mode 100644 tests/fixtures/parity/expected/verify__select_no_selection_pending_409.json create mode 100644 tests/fixtures/parity/expected/verify__select_too_many_422.json create mode 100644 tests/fixtures/parity/expected/verify__select_unknown_task_404.json create mode 100644 tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json create mode 100644 tests/fixtures/parity/expected/verify__status_completed.json create mode 100644 tests/fixtures/parity/expected/verify__status_completed_after_a_while.json create mode 100644 tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/expected/verify__status_completed_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/expected/verify__status_completed_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/expected/verify__status_failed.json create mode 100644 tests/fixtures/parity/expected/verify__status_failed_after_a_while.json create mode 100644 tests/fixtures/parity/expected/verify__status_failed_after_a_while_framing.json create mode 100644 tests/fixtures/parity/expected/verify__status_failed_retryable.json create mode 100644 tests/fixtures/parity/expected/verify__status_needs_input.json create mode 100644 tests/fixtures/parity/expected/verify__status_not_a_claim.json create mode 100644 tests/fixtures/parity/expected/verify__status_not_a_claim_after_a_while.json create mode 100644 tests/fixtures/parity/expected/verify__status_not_yours_404.json create mode 100644 tests/fixtures/parity/expected/verify__status_older_run_completed.json create mode 100644 tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json create mode 100644 tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json create mode 100644 tests/fixtures/parity/expected/verify__status_older_run_in_progress.json create mode 100644 tests/fixtures/parity/expected/verify__status_processing.json create mode 100644 tests/fixtures/parity/expected/verify__status_processing_after_a_while.json create mode 100644 tests/fixtures/parity/expected/verify__status_task_stuck.json create mode 100644 tests/fixtures/parity/expected/verify__status_unknown_404.json create mode 100644 tests/fixtures/parity/expected/verify__submit_202.json create mode 100644 tests/fixtures/parity/expected/verify__submit_202_options.json create mode 100644 tests/fixtures/parity/expected/verify__submit_202_text_alias.json create mode 100644 tests/fixtures/parity/expected/verify__unauthenticated_401.json create mode 100644 tests/fixtures/parity/expected/verify__verification_200.json create mode 100644 tests/fixtures/parity/expected/verify__verification_200_covered.json create mode 100644 tests/fixtures/parity/expected/verify__verification_200_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/expected/verify__verification_200_modified_at_null.json create mode 100644 tests/fixtures/parity/expected/verify__verification_200_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/expected/verify__verification_200_modified_at_set.json create mode 100644 tests/fixtures/parity/expected/verify__verification_200_result_json_conclusion_only.json create mode 100644 tests/fixtures/parity/expected/verify__verification_200_result_json_empty.json create mode 100644 tests/fixtures/parity/expected/verify__verification_200_result_json_old_shape_strings.json create mode 100644 tests/fixtures/parity/expected/verify__verification_404.json create mode 100644 tests/fixtures/parity/expected/verify__verification_by_task_id_200.json create mode 100644 tests/fixtures/parity/expected/verify__verification_failed_409.json create mode 100644 tests/fixtures/parity/expected/verify__verification_failed_409_not_a_claim_after_a_while.json create mode 100644 tests/fixtures/parity/expected/verify__verification_not_ready_409.json create mode 100644 tests/fixtures/parity/expected/verify__verification_not_yours_404.json create mode 100644 tests/fixtures/parity/expected/verify__verification_public_anonymous.json create mode 100644 tests/fixtures/parity/expected/verify__verification_public_of_another_user.json create mode 100644 tests/fixtures/parity/expected/verify__verification_unknown_task_404.json create mode 100644 tests/fixtures/parity/expected/verify__verification_zero_retention_expired_410.json create mode 100644 tests/fixtures/parity/expected/verify__verification_zero_retention_served.json create mode 100644 tests/fixtures/parity/expected/webhook__certificate_timestamped.json create mode 100644 tests/fixtures/parity/expected/webhook__certificate_timestamped_review_child.json create mode 100644 tests/fixtures/parity/expected/webhook__citecheck_completed.json create mode 100644 tests/fixtures/parity/expected/webhook__citecheck_failed.json create mode 100644 tests/fixtures/parity/expected/webhook__review_completed.json create mode 100644 tests/fixtures/parity/expected/webhook__review_completed_key_default_url.json create mode 100644 tests/fixtures/parity/expected/webhook__review_completed_oversized_rebuilt.json create mode 100644 tests/fixtures/parity/expected/webhook__review_failed.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_completed.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_completed_in_batch.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_completed_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_failed_insufficient_evidence.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_failed_not_a_claim.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_failed_upstream_unavailable.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_needs_input_multi_claim.json create mode 100644 tests/fixtures/parity/legacy/account__library_empty.json create mode 100644 tests/fixtures/parity/legacy/account__library_item_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/legacy/account__library_item_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/legacy/account__library_items.json create mode 100644 tests/fixtures/parity/legacy/account__library_no_match.json create mode 100644 tests/fixtures/parity/legacy/account__library_page_past_end.json create mode 100644 tests/fixtures/parity/legacy/account__library_paginated_second_page.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_extra_credits.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_extra_only.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_extract_calls.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_free.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_free_partly_spent.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_plus.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_pro.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_pro_extra.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_stale_quota_row.json create mode 100644 tests/fixtures/parity/legacy/assess__401_no_key.json create mode 100644 tests/fixtures/parity/legacy/assess__402_no_credits.json create mode 100644 tests/fixtures/parity/legacy/assess__422_blank_item.json create mode 100644 tests/fixtures/parity/legacy/assess__422_blank_text.json create mode 100644 tests/fixtures/parity/legacy/assess__422_input_conflict.json create mode 100644 tests/fixtures/parity/legacy/assess__422_item_too_long.json create mode 100644 tests/fixtures/parity/legacy/assess__422_no_input_field.json create mode 100644 tests/fixtures/parity/legacy/assess__422_too_many_items.json create mode 100644 tests/fixtures/parity/legacy/assess__422_wrong_type_claim.json create mode 100644 tests/fixtures/parity/legacy/assess__422_wrong_type_claims.json create mode 100644 tests/fixtures/parity/legacy/assess__502_framing_failed.json create mode 100644 tests/fixtures/parity/legacy/assess__503_capacity.json create mode 100644 tests/fixtures/parity/legacy/assess__503_list_all_upstream_unavailable.json create mode 100644 tests/fixtures/parity/legacy/assess__503_upstream_unavailable.json create mode 100644 tests/fixtures/parity/legacy/assess__deep_tier_hit.json create mode 100644 tests/fixtures/parity/legacy/assess__idempotency_body_mismatch.json create mode 100644 tests/fixtures/parity/legacy/assess__idempotency_first.json create mode 100644 tests/fixtures/parity/legacy/assess__idempotency_replay.json create mode 100644 tests/fixtures/parity/legacy/assess__list_all_error_rows.json create mode 100644 tests/fixtures/parity/legacy/assess__list_compound_item.json create mode 100644 tests/fixtures/parity/legacy/assess__list_mixed_rows.json create mode 100644 tests/fixtures/parity/legacy/assess__memo_hit.json create mode 100644 tests/fixtures/parity/legacy/assess__replay_later_200.json create mode 100644 tests/fixtures/parity/legacy/assess__single_no_claim.json create mode 100644 tests/fixtures/parity/legacy/assess__single_one_claim.json create mode 100644 tests/fixtures/parity/legacy/assess__single_text_over_wave_more_claims.json create mode 100644 tests/fixtures/parity/legacy/assess__single_text_several_claims.json create mode 100644 tests/fixtures/parity/legacy/assess__suggest_rewrite.json create mode 100644 tests/fixtures/parity/legacy/citecheck__402_no_credits.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_empty_body.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_invalid_doi.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_max_citations_over_20.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_max_citations_with_pairs.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_pair_bad_url_scheme.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_pair_no_source.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_pair_url_and_doi.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_pairs_empty.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_pairs_over_20.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_quote_not_in_statement.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_text_and_pairs.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_unknown_field.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_url_input.json create mode 100644 tests/fixtures/parity/legacy/citecheck__429_citecheck_in_flight.json create mode 100644 tests/fixtures/parity/legacy/citecheck__503_capacity.json create mode 100644 tests/fixtures/parity/legacy/citecheck__503_citations_unavailable.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_404_not_found.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_404_review_id.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_410_purged.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_checking.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_completed_clean.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_completed_doi_pair.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_completed_incomplete.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_completed_issues_found.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_completed_partly_supported.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_completed_unchecked.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_failed_no_citations.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_failed_upstream_unavailable.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_queued.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_text_limit_exactly_at_limit.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_text_limit_reached.json create mode 100644 tests/fixtures/parity/legacy/citecheck__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/legacy/citecheck__idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/legacy/citecheck__idempotent_replay_202.json create mode 100644 tests/fixtures/parity/legacy/citecheck__receipt_202.json create mode 100644 tests/fixtures/parity/legacy/citecheck__receipt_202_empty_webhook_url.json create mode 100644 tests/fixtures/parity/legacy/errors__ask_bad_language.json create mode 100644 tests/fixtures/parity/legacy/errors__ask_empty_message.json create mode 100644 tests/fixtures/parity/legacy/errors__ask_failed.json create mode 100644 tests/fixtures/parity/legacy/errors__ask_idempotency_key_reused.json create mode 100644 tests/fixtures/parity/legacy/errors__ask_message_too_long.json create mode 100644 tests/fixtures/parity/legacy/errors__ask_not_completed.json create mode 100644 tests/fixtures/parity/legacy/errors__ask_reset_not_found.json create mode 100644 tests/fixtures/parity/legacy/errors__ask_send_not_found.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_bad_key.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_basic_scheme.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_insufficient_scope.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_missing_bearer.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_missing_bearer_post.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_private_claim_ask_history.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_private_claim_ask_send.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_unrecognised_prefix.json create mode 100644 tests/fixtures/parity/legacy/errors__http_error_library_bad_curated.json create mode 100644 tests/fixtures/parity/legacy/errors__http_error_library_bad_verdict.json create mode 100644 tests/fixtures/parity/legacy/errors__not_found_ask.json create mode 100644 tests/fixtures/parity/legacy/errors__not_found_route.json create mode 100644 tests/fixtures/parity/legacy/errors__not_found_verification.json create mode 100644 tests/fixtures/parity/legacy/errors__not_found_verify_status.json create mode 100644 tests/fixtures/parity/legacy/errors__payment_required_ask.json create mode 100644 tests/fixtures/parity/legacy/errors__payment_required_assess.json create mode 100644 tests/fixtures/parity/legacy/errors__payment_required_verify.json create mode 100644 tests/fixtures/parity/legacy/errors__payment_required_verify_batch_short.json create mode 100644 tests/fixtures/parity/legacy/errors__rate_limited_extract.json create mode 100644 tests/fixtures/parity/legacy/errors__service_unavailable_ask.json create mode 100644 tests/fixtures/parity/legacy/errors__service_unavailable_capacity.json create mode 100644 tests/fixtures/parity/legacy/errors__validation_malformed_json.json create mode 100644 tests/fixtures/parity/legacy/errors__validation_missing_field_hint.json create mode 100644 tests/fixtures/parity/legacy/errors__validation_unknown_field_only.json create mode 100644 tests/fixtures/parity/legacy/errors__validation_unknown_field_wrong_type.json create mode 100644 tests/fixtures/parity/legacy/errors__validation_wrong_type.json create mode 100644 tests/fixtures/parity/legacy/extract__401_no_key.json create mode 100644 tests/fixtures/parity/legacy/extract__422_blank_text.json create mode 100644 tests/fixtures/parity/legacy/extract__422_focus_too_long.json create mode 100644 tests/fixtures/parity/legacy/extract__422_missing_text.json create mode 100644 tests/fixtures/parity/legacy/extract__429_daily_limit.json create mode 100644 tests/fixtures/parity/legacy/extract__502_extraction_failed.json create mode 100644 tests/fixtures/parity/legacy/extract__503_upstream_unavailable.json create mode 100644 tests/fixtures/parity/legacy/extract__focus_ready.json create mode 100644 tests/fixtures/parity/legacy/extract__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/legacy/extract__idempotency_replay.json create mode 100644 tests/fixtures/parity/legacy/extract__locate_all_dropped.json create mode 100644 tests/fixtures/parity/legacy/extract__locate_failed.json create mode 100644 tests/fixtures/parity/legacy/extract__locate_leading_whitespace.json create mode 100644 tests/fixtures/parity/legacy/extract__locate_some_dropped.json create mode 100644 tests/fixtures/parity/legacy/extract__locate_true.json create mode 100644 tests/fixtures/parity/legacy/extract__no_match_with_focus.json create mode 100644 tests/fixtures/parity/legacy/extract__not_a_claim.json create mode 100644 tests/fixtures/parity/legacy/extract__not_a_claim_beside_claims.json create mode 100644 tests/fixtures/parity/legacy/extract__ready_one_claim.json create mode 100644 tests/fixtures/parity/legacy/extract__ready_several_claims.json create mode 100644 tests/fixtures/parity/legacy/extract__replay_later_200.json create mode 100644 tests/fixtures/parity/legacy/extract__url_fetch_502_unreadable.json create mode 100644 tests/fixtures/parity/legacy/extract__url_fetch_503_upstream.json create mode 100644 tests/fixtures/parity/legacy/extract__url_input_located.json create mode 100644 tests/fixtures/parity/legacy/review__401_no_credentials.json create mode 100644 tests/fixtures/parity/legacy/review__402_no_credits.json create mode 100644 tests/fixtures/parity/legacy/review__402_no_credits_exhausted.json create mode 100644 tests/fixtures/parity/legacy/review__422_blank_text.json create mode 100644 tests/fixtures/parity/legacy/review__422_citations_options_object.json create mode 100644 tests/fixtures/parity/legacy/review__422_depth_unknown.json create mode 100644 tests/fixtures/parity/legacy/review__422_invalid_confidence_band.json create mode 100644 tests/fixtures/parity/legacy/review__422_invalid_verdict_label.json create mode 100644 tests/fixtures/parity/legacy/review__422_max_assessments_negative.json create mode 100644 tests/fixtures/parity/legacy/review__422_max_citations_negative.json create mode 100644 tests/fixtures/parity/legacy/review__422_max_citations_too_big.json create mode 100644 tests/fixtures/parity/legacy/review__422_max_verifications_too_big.json create mode 100644 tests/fixtures/parity/legacy/review__422_text_missing.json create mode 100644 tests/fixtures/parity/legacy/review__422_text_wrong_type.json create mode 100644 tests/fixtures/parity/legacy/review__422_unknown_escalate_field.json create mode 100644 tests/fixtures/parity/legacy/review__422_unknown_top_level_field.json create mode 100644 tests/fixtures/parity/legacy/review__422_unsupported_language.json create mode 100644 tests/fixtures/parity/legacy/review__422_visibility_public.json create mode 100644 tests/fixtures/parity/legacy/review__429_extract_daily_limit.json create mode 100644 tests/fixtures/parity/legacy/review__429_review_in_flight.json create mode 100644 tests/fixtures/parity/legacy/review__503_capacity.json create mode 100644 tests/fixtures/parity/legacy/review__503_citations_capacity.json create mode 100644 tests/fixtures/parity/legacy/review__get_403_insufficient_scope.json create mode 100644 tests/fixtures/parity/legacy/review__get_404_not_found.json create mode 100644 tests/fixtures/parity/legacy/review__get_404_other_account.json create mode 100644 tests/fixtures/parity/legacy/review__get_410_purged.json create mode 100644 tests/fixtures/parity/legacy/review__get_422_unknown_view.json create mode 100644 tests/fixtures/parity/legacy/review__get_assessing.json create mode 100644 tests/fixtures/parity/legacy/review__get_assessment_rows_full_fields.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_clean.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_failed_and_unchecked.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_issue_full.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_issue_issues_view.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_limit_exactly_at_limit.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_limit_reached.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_no_claim_but_citations.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_no_claim_no_citation.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_not_asked.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_only.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_partly_supported.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_running.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_skipped_switched_off.json create mode 100644 tests/fixtures/parity/legacy/review__get_citations_skipped_url_input.json create mode 100644 tests/fixtures/parity/legacy/review__get_claim_limit_not_reached.json create mode 100644 tests/fixtures/parity/legacy/review__get_claim_limit_reached_exact.json create mode 100644 tests/fixtures/parity/legacy/review__get_completed_assess_only.json create mode 100644 tests/fixtures/parity/legacy/review__get_completed_clean.json create mode 100644 tests/fixtures/parity/legacy/review__get_completed_issues_all_verified.json create mode 100644 tests/fixtures/parity/legacy/review__get_completed_issues_full.json create mode 100644 tests/fixtures/parity/legacy/review__get_completed_issues_view_issues.json create mode 100644 tests/fixtures/parity/legacy/review__get_completed_positions.json create mode 100644 tests/fixtures/parity/legacy/review__get_completed_recovered_claim.json create mode 100644 tests/fixtures/parity/legacy/review__get_completed_with_untraced_claim.json create mode 100644 tests/fixtures/parity/legacy/review__get_during_quick_check.json create mode 100644 tests/fixtures/parity/legacy/review__get_failed_every_assessment_failed.json create mode 100644 tests/fixtures/parity/legacy/review__get_failed_no_claim.json create mode 100644 tests/fixtures/parity/legacy/review__get_failed_no_claim_untraced.json create mode 100644 tests/fixtures/parity/legacy/review__get_more_claims.json create mode 100644 tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/legacy/review__get_policy_resolved.json create mode 100644 tests/fixtures/parity/legacy/review__get_queued.json create mode 100644 tests/fixtures/parity/legacy/review__get_suggested_edits.json create mode 100644 tests/fixtures/parity/legacy/review__get_suggested_edits_issues_view.json create mode 100644 tests/fixtures/parity/legacy/review__get_url_review.json create mode 100644 tests/fixtures/parity/legacy/review__get_verifying.json create mode 100644 tests/fixtures/parity/legacy/review__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/legacy/review__idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/legacy/review__idempotency_conflict_409_existing_review.json create mode 100644 tests/fixtures/parity/legacy/review__idempotent_replay_202.json create mode 100644 tests/fixtures/parity/legacy/review__receipt_202.json create mode 100644 tests/fixtures/parity/legacy/review__receipt_202_empty_webhook_url.json create mode 100644 tests/fixtures/parity/legacy/review__receipt_202_webhook_url.json create mode 100644 tests/fixtures/parity/legacy/review__replay_later_202.json create mode 100644 tests/fixtures/parity/legacy/verify__batch_202.json create mode 100644 tests/fixtures/parity/legacy/verify__batch_capacity_503.json create mode 100644 tests/fixtures/parity/legacy/verify__batch_empty_422.json create mode 100644 tests/fixtures/parity/legacy/verify__batch_idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/legacy/verify__batch_item_missing_claim_422.json create mode 100644 tests/fixtures/parity/legacy/verify__batch_no_credits_402.json create mode 100644 tests/fixtures/parity/legacy/verify__batch_partial_202.json create mode 100644 tests/fixtures/parity/legacy/verify__batch_too_many_422.json create mode 100644 tests/fixtures/parity/legacy/verify__blank_claim_422.json create mode 100644 tests/fixtures/parity/legacy/verify__capacity_503.json create mode 100644 tests/fixtures/parity/legacy/verify__idempotency_body_mismatch_422.json create mode 100644 tests/fixtures/parity/legacy/verify__idempotency_conflict_409.json create mode 100644 tests/fixtures/parity/legacy/verify__idempotency_key_replay.json create mode 100644 tests/fixtures/parity/legacy/verify__implicit_dedup_200.json create mode 100644 tests/fixtures/parity/legacy/verify__implicit_repeat_replay.json create mode 100644 tests/fixtures/parity/legacy/verify__invalid_depth_422.json create mode 100644 tests/fixtures/parity/legacy/verify__invalid_language_422.json create mode 100644 tests/fixtures/parity/legacy/verify__list_200.json create mode 100644 tests/fixtures/parity/legacy/verify__list_200_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/legacy/verify__list_200_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/legacy/verify__list_clamped_200.json create mode 100644 tests/fixtures/parity/legacy/verify__list_empty_200.json create mode 100644 tests/fixtures/parity/legacy/verify__list_page2_200.json create mode 100644 tests/fixtures/parity/legacy/verify__misnamed_field_422.json create mode 100644 tests/fixtures/parity/legacy/verify__missing_claim_422.json create mode 100644 tests/fixtures/parity/legacy/verify__no_credits_402.json create mode 100644 tests/fixtures/parity/legacy/verify__replay_later_202.json create mode 100644 tests/fixtures/parity/legacy/verify__select_202.json create mode 100644 tests/fixtures/parity/legacy/verify__select_empty_422.json create mode 100644 tests/fixtures/parity/legacy/verify__select_invalid_selection_422.json create mode 100644 tests/fixtures/parity/legacy/verify__select_no_selection_pending_409.json create mode 100644 tests/fixtures/parity/legacy/verify__select_too_many_422.json create mode 100644 tests/fixtures/parity/legacy/verify__select_unknown_task_404.json create mode 100644 tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json create mode 100644 tests/fixtures/parity/legacy/verify__status_completed.json create mode 100644 tests/fixtures/parity/legacy/verify__status_completed_after_a_while.json create mode 100644 tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/legacy/verify__status_completed_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/legacy/verify__status_completed_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/legacy/verify__status_failed.json create mode 100644 tests/fixtures/parity/legacy/verify__status_failed_after_a_while.json create mode 100644 tests/fixtures/parity/legacy/verify__status_failed_after_a_while_framing.json create mode 100644 tests/fixtures/parity/legacy/verify__status_failed_retryable.json create mode 100644 tests/fixtures/parity/legacy/verify__status_needs_input.json create mode 100644 tests/fixtures/parity/legacy/verify__status_not_a_claim.json create mode 100644 tests/fixtures/parity/legacy/verify__status_not_a_claim_after_a_while.json create mode 100644 tests/fixtures/parity/legacy/verify__status_not_yours_404.json create mode 100644 tests/fixtures/parity/legacy/verify__status_older_run_completed.json create mode 100644 tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json create mode 100644 tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json create mode 100644 tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json create mode 100644 tests/fixtures/parity/legacy/verify__status_processing.json create mode 100644 tests/fixtures/parity/legacy/verify__status_processing_after_a_while.json create mode 100644 tests/fixtures/parity/legacy/verify__status_task_stuck.json create mode 100644 tests/fixtures/parity/legacy/verify__status_unknown_404.json create mode 100644 tests/fixtures/parity/legacy/verify__submit_202.json create mode 100644 tests/fixtures/parity/legacy/verify__submit_202_options.json create mode 100644 tests/fixtures/parity/legacy/verify__submit_202_text_alias.json create mode 100644 tests/fixtures/parity/legacy/verify__unauthenticated_401.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_200.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_200_covered.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_200_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_200_modified_at_null.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_200_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_200_modified_at_set.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_200_result_json_conclusion_only.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_200_result_json_empty.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_200_result_json_old_shape_strings.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_404.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_by_task_id_200.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_failed_409.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_failed_409_not_a_claim_after_a_while.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_not_ready_409.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_not_yours_404.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_public_anonymous.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_public_of_another_user.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_unknown_task_404.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_zero_retention_expired_410.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_zero_retention_served.json create mode 100644 tests/fixtures/parity/legacy/webhook__certificate_timestamped.json create mode 100644 tests/fixtures/parity/legacy/webhook__certificate_timestamped_review_child.json create mode 100644 tests/fixtures/parity/legacy/webhook__citecheck_completed.json create mode 100644 tests/fixtures/parity/legacy/webhook__citecheck_failed.json create mode 100644 tests/fixtures/parity/legacy/webhook__review_completed.json create mode 100644 tests/fixtures/parity/legacy/webhook__review_completed_key_default_url.json create mode 100644 tests/fixtures/parity/legacy/webhook__review_completed_oversized_rebuilt.json create mode 100644 tests/fixtures/parity/legacy/webhook__review_failed.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_completed.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_completed_in_batch.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_same_day_hours_apart.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_failed_insufficient_evidence.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_failed_not_a_claim.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_failed_upstream_unavailable.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_needs_input_multi_claim.json create mode 100644 tests/fixtures/parity/requests.json create mode 100644 tests/parity_generate.py create mode 100644 tests/parity_observe.py create mode 100644 tests/parity_requests.py create mode 100644 tests/test_parity.py create mode 100644 tests/test_read_both_shapes.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 058d2e9..17abab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,82 @@ All notable changes to this SDK are documented here. Format follows ## [Unreleased] +Minor release (2.21.0). Existing code keeps working unchanged; nothing to do +on upgrade. + +### Added + +- **Reads both response shapes.** The API is adding a newer, dated response + shape that gives each field one name across every endpoint. This release + still asks for the original shape (it sends the same `X-Lenz-API-Version` + as before), and every model now reads either shape. The newer names are + attributes now, filled from whichever shape arrives: + - `ExtractedClaims.claims`: every claim found, always a list, each an + `ExtractedClaim` with `claim` and (with `locate=True`) `positions`. + - `AssessClaim.status` (`"completed"` / `"failed"`), `AssessClaim.failure` + (`code`, `detail`, `hint`, `failure_class`, `retryable`, `docs_url`) and + `AssessClaim.more_claims`; `AssessResponse.status` (`ok` / + `no_checkable_claim` / `error`, the `AssessStatus` constant) and + `AssessResponse.failure`. + - `TaskStatus.failure`, the same block, on a failed verification. + - `TaskAccepted.claim`, `BatchItemResult.claim`, `CandidateClaim.claim`. + - `completed_at` on `Verification`, `VerificationListItem` and + `ReviewVerification`. + - `FailureBlock.code` and `FailureBlock.detail`; + `ReviewAssessment.more_claims`; `ReviewSummary.claims_found`, + `claim_limit_exceeded` and `citation_limit_exceeded`; + `CitecheckSummary.citation_limit_exceeded`. + - Webhooks: `event_id` on every event, `VerificationFailed.failure`, + `VerificationNeedsInput.reason` and `.claims`. `parse_webhook` and + `LenzWebhooks.parse` read the newer envelope too (`event`, `event_id`, + the work's id, `status`, and the polled body under `verification` / + `review` / `citecheck`). + - Errors: a 409 for a failed run, a 429 and a 422 are read in either shape + (`failure` block, `retry_after`, `errors` list with a sentence `detail`). + +### Deprecated + +- The older names, kept with the meaning they always had, whichever shape + arrives: `ExtractedClaims.claim` / `identified_claims` / `locations`; + `AssessClaim.error_code` / `hint` / `identified_claims` (a failed row still + reads `verdict == "Error"` and `confidence == "low"`); `AssessResponse.error` + / `error_code`; `TaskStatus.error` / `failure_reason` / `failure_detail`; + `claim_text` and `CandidateClaim.text`; `modified_at` (computed from + `completed_at` with its original rule: set only when the verification + completed on a later UTC calendar day than it was created); + `FailureBlock.failure_reason`; `ReviewAssessment.error_code` / `hint` / + `identified_claims`; `claim_limit_reached` / `citation_limit_reached`; + `Usage.quota_resets_at` and the `verify` / `ask` / `assess` blocks (computed + from `credits` and `costs` when a response leaves them out). They are marked + deprecated in the JSON schema; reading them does not warn. "Nothing + checkable" keeps its old spelling in the old fields (`not_a_claim`, + `no_claim`) and reads `no_checkable_claim` in the new ones; + `ExtractedClaims.status` keeps `not_a_claim`. + +### Changed + +- **`verify`, `verify_and_wait`, `verify_batch` and `verify_batch_and_wait` + no longer send `webhook_url: ""`** when no webhook URL was given (nor an + empty per-item `webhook_url`). The API has always read an empty value there + as "use the key's default webhook", the same as leaving it out, so nothing + changes now; leaving it out keeps that meaning on later API versions, where + `""` means "no webhook". A retry that reuses an idempotency key from an + earlier release's call sends a different body and is refused as a + different request. `review` and `citecheck` send `webhook_url` exactly as + before (there `""` means "no webhook"). +- `model_dump()` returns the shape the server sent: attributes filled in from + the other shape are left out, so an original-shape response dumps exactly as + before. + +### Correction + +- **The /me/usage fields are not removed on 2026-11-29.** Earlier entries + (2.9.0 and 2.14.0) said `UsageCredits.bonus`, `UsageCapacity.credits` and the + per-capability `verify` / `ask` / `assess` blocks would go on that date. + They are deprecated and kept for existing callers; the API keeps sending + them to integrations built against its original shape. Their deprecation + warnings no longer name a date. + ## [2.20.0] - 2026-10-05 ### Added diff --git a/README.md b/README.md index efc0025..502a192 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ edited = "".join(chars) `poll_after_seconds`. Pass `on_update=` to see the quick verdicts as soon as they are in and each deep check as it lands; without it the helper is silent. It raises `ReviewFailed` when the review fails (`error_code` and `hint` say -why) and `ReviewTimeout` after `timeout` seconds (600 by default); the review +why; `review.failure.code` is the cause in the newer spelling) and `ReviewTimeout` after `timeout` seconds (600 by default); the review keeps running, and the error carries its `review_id` and the last body read. To submit without waiting, `client.review(draft)` returns a `review_id`; read it with `client.get_review(review_id)`, or only the issues with @@ -297,7 +297,7 @@ client = Lenz(api_key="lenz_...") # 1. extract — pull verifiable claims out of any text (free) # add focus="..." to narrow it to the claims you care about out = client.extract(text=llm_output) -claims = out.identified_claims or [out.claim] +claims = [c.claim for c in out.claims] # 2. assess — one call per 20 claims (extract finds up to 100), one row per claim, same order quick = [row for i in range(0, len(claims), 20) for row in client.assess(claims=claims[i : i + 20]).claims] @@ -308,7 +308,7 @@ for c in quick: # 3. verify — escalate the low-confidence rows to the full panel + citations # verify_batch_and_wait takes up to 20 claims a call: the first 20 here -doubtful = [{"claim": c.claim} for c in quick if c.verdict != "Error" and c.confidence == "low"][:20] +doubtful = [{"claim": c.claim} for c in quick if c.status == "completed" and c.confidence == "low"][:20] results = client.verify_batch_and_wait(claims=doubtful) if doubtful else [] for r in results: if r.verification: @@ -322,13 +322,13 @@ print(reply.content) `assess(claims=[...])` takes up to 20 claims per call and always answers with exactly one row per claim, in the order sent. A row that could not be -given a verdict comes back in position with `verdict == "Error"`, an -`error_code` (`no_claim` / `framing_failed` / `upstream_unavailable` / -`timeout` — an open set; the last two are the ones worth resending as-is) -and a one-sentence `hint` on what to send next; it is not charged. A -compound item is assessed on its main claim and lists the other claims it -found in `identified_claims` (also with a `hint`) — send those as their own -items to check the rest. `assess(claim="...")` takes one text and answers +given a verdict comes back in position with `status == "failed"` and a +`failure` block: `failure.code` (`no_checkable_claim` / `framing_failed` / +`upstream_unavailable` / `timeout` — an open set; the last two are the ones +worth resending as-is) and `failure.hint`, one sentence on what to send next; +it is not charged. A compound item is assessed on its main claim and lists the +other claims it found in `more_claims` — send those as their own items to +check the rest. `assess(claim="...")` takes one text and answers with a row per claim found in it, up to 20, at 1 credit each; a text that makes more claims gets its 20 most check-worthy checked and the rest in `more_claims`, unchecked and free — send them back as `claims`, 20 a call. @@ -387,8 +387,8 @@ your own claims. Use webhooks for production async flows. ## What you get on the client -- **`client.extract(text=...)`** → `ExtractedClaims`. Free, capped at 1000/account/day. Add `focus=` to narrow the list — see [Steering extract](#steering-extract) — and `locate=True` to keep only the claims traced back to your text, with their positions (`out.locations`). Each attempt waits up to 150s by default (a timeout is retried like any transport error, and the call's idempotency key makes the retry replay the first answer); `timeout=` overrides it for that call. -- **`client.assess(claim=...)`** / **`client.assess(claims=[...])`** → `AssessResponse`. Sync. One statement (~15s; `text=` is accepted as an alias: a document is `text`, a claim is `claim`) or a list of up to 20 claims in one call (~15s) — exactly one row per claim, in order; rows that got no verdict are `"Error"` rows with an `error_code` and a `hint`, in position and free. A single text past 20 claims lists the rest in `more_claims` (`[]` otherwise). The two forms are mutually exclusive. `timeout=` overrides the client timeout for that call (both forms default to 100s: a long text can take up to 90s). +- **`client.extract(text=...)`** → `ExtractedClaims`. Free, capped at 1000/account/day. Add `focus=` to narrow the list — see [Steering extract](#steering-extract) — and `locate=True` to keep only the claims traced back to your text, with their positions (each `out.claims[i].positions`). Each attempt waits up to 150s by default (a timeout is retried like any transport error, and the call's idempotency key makes the retry replay the first answer); `timeout=` overrides it for that call. +- **`client.assess(claim=...)`** / **`client.assess(claims=[...])`** → `AssessResponse`. Sync. One statement (~15s; `text=` is accepted as an alias: a document is `text`, a claim is `claim`) or a list of up to 20 claims in one call (~15s) — exactly one row per claim, in order; rows that got no verdict have `status == "failed"` and a `failure` block (`failure.code`, `failure.hint`), in position and free. A single text past 20 claims lists the rest in `more_claims` (`[]` otherwise). The two forms are mutually exclusive. `timeout=` overrides the client timeout for that call (both forms default to 100s: a long text can take up to 90s). - **`client.verify(...)`** → `TaskAccepted`. Async submit; returns a `task_id`. Get the result by polling (`client.wait(...)` / `client.get_status(...)`) or via a webhook. - **`client.verify_and_wait(...)`** → `Verification`. Submit + poll until the pipeline lands (sync ergonomic). Equivalent to `wait(verify(...))`. - **`client.wait(task)`** → `Verification`. Block on a `task_id` (or a `TaskAccepted`) until it terminates. The polling counterpart to a webhook. @@ -426,9 +426,9 @@ results = client.verify_batch_and_wait( ) for r in results: if r.status == "completed": - print(r.claim_text, "→", r.verification.verdict) + print(r.claim, "→", r.verification.verdict) else: - print(r.claim_text, "→", r.status) # needs_input | failed | timeout + print(r.claim, "→", r.status) # needs_input | failed | timeout ``` A `failed` item with `status_detail is None` is a verification its account's @@ -471,6 +471,30 @@ Every claim-shaped response shares these fields at top level: | `confidence` | `str` | Categorical: `"high"` \| `"medium"` \| `"low"`. | | `lenz_score` | `int \| None` | Integer 1–10 (deep verdicts and list endpoints; `assess` omits it). | +### Field names: both response shapes + +The API is adding a newer, dated response shape that gives each field one +name across every endpoint. This SDK still asks for the original shape, and +its models read either one. The newer names are attributes already; the +older ones keep working, with the meaning they always had: + +| Read this | Instead of (deprecated, still works) | +|---|---| +| `ExtractedClaims.claims` (each `.claim`, `.positions`) | `claim`, `identified_claims`, `locations` | +| `AssessClaim.status` (`"completed"` / `"failed"`) and `.failure` | `verdict == "Error"`, `error_code`, `hint` | +| `AssessClaim.more_claims`, `ReviewAssessment.more_claims` | `identified_claims` | +| `AssessResponse.status` and `.failure` | `error`, `error_code` | +| `TaskStatus.failure` (`code`, `detail`, `hint`, `failure_class`, `retryable`, `docs_url`) | `error`, `failure_reason` and the flat fields | +| `TaskAccepted.claim`, `BatchItemResult.claim`, `CandidateClaim.claim` | `claim_text`, `text` | +| `Verification.completed_at` | `modified_at` | +| `ReviewSummary.claim_limit_exceeded`, `citation_limit_exceeded` | `claim_limit_reached`, `citation_limit_reached` | +| `FailureBlock.code`, `.detail` | `failure_reason` | +| `Usage.credits` and `Usage.costs` | the `verify` / `ask` / `assess` blocks, `quota_resets_at` | + +"Nothing checkable" is `no_checkable_claim` in the newer names; the older +fields keep their own spelling (`not_a_claim`, `no_claim`). `model_dump()` +returns the shape the server sent. + ### A suggested rewrite (`suggested_rewrite`) A verification can carry `suggested_rewrite`: a suggested rewrite of its @@ -644,13 +668,13 @@ them. `credits.extra` is the non-expiring part of the balance. Its old name, `credits.bonus`, is deprecated: the same number, it emits a -`DeprecationWarning` when read and goes away on 2026-11-29. +`DeprecationWarning` when read and is kept for existing code. Per-capability `bonus` is that capability's share of `credits.extra`, so 200 extra credits read as `assess.bonus == 200` and `verify.bonus == 20`. The old `capability.credits` field is a deprecated alias of `bonus` (it never meant -the pool); reading it emits a `DeprecationWarning` and it goes away on -2026-11-29. +the pool); reading it emits a `DeprecationWarning` and it is kept for +existing code. ### Depth pricing @@ -815,7 +839,7 @@ At most 300 characters. A longer focus is rejected with a 422 rather than truncated, so you never get a subset you did not ask for. When the document has claims but none fall within your focus, `status` is -`"no_match"` and `identified_claims` is empty. The unfocused list is never +`"no_match"` and `claims` is empty. The unfocused list is never substituted — widen the focus and call again. ```python @@ -838,27 +862,24 @@ to your text, and to learn where the text makes each one: ```python out = client.extract(text=draft, locate=True) -for loc in out.locations or []: - for pos in loc.positions or []: - print(loc.claim, "->", pos.text) +for c in out.claims: + for pos in c.positions or []: + print(c.claim, "->", pos.text) if pos.start is not None: assert draft[pos.start : pos.end] == pos.text ``` A claim found nowhere in the text, or found with a different figure, is left -out; if that leaves no claim, `status` is `"not_a_claim"`. `locations` has one -`ClaimLocation` per returned claim, in the order of `identified_claims` (one -entry for a single `claim`), each with its `positions` (every place the text -makes it, in text order, at least one and at most 10). Each is a `Position`: +out; if that leaves no claim, `status` is `"not_a_claim"`. Each entry of +`claims` has its `positions` (every place the text makes it, in text order, +at least one and at most 10). Each is a `Position`: `start` and `end` index the text you sent in Unicode code points, so `text[start:end]` works natively; `end` is exclusive. Both are `None` when the input was a URL, since the page is not returned; `pos.text` carries the passage. -`locations` is `[]` when every claim was left out (`status` is then -`"not_a_claim"`), and `None` when `locate` was not set, when the extraction -found no claims, or -when the claims could not be located — the list is then returned unfiltered. +`positions` is `None` when `locate` was not set, or when the claims could not +be located — the list is then returned unfiltered. Locating adds a few seconds. `locate` defaults to off; leave it `None` to use the server default, or pass `False` to turn it off explicitly. diff --git a/openapi.json b/openapi.json index f53bc7d..97c8a8b 100644 --- a/openapi.json +++ b/openapi.json @@ -411,7 +411,7 @@ } } }, - "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\n``credits.bonus`` is the deprecated old name of ``credits.extra``, the same\nnumber, removed on **2026-11-29** with the fields below. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15 (#664), while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated.** ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nThey go on **2026-11-29**, together with the per-block ``credits`` alias.\nDerive them instead::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias, on the same date.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.", + "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\n``credits.bonus`` is the deprecated old name of ``credits.extra``, the same\nnumber, deprecated with the fields below and kept for existing callers. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15 (#664), while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated.** ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nThey are deprecated, together with the per-block ``credits`` alias, and\nkept for existing callers. Derive them instead::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias, on the same date.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.", "tags": [ "Account" ], @@ -2510,7 +2510,7 @@ "type": "object" }, "MeUsageCreditsOut": { - "description": "The account's credit balance \u2014 the one pool every capability spends.\n\nTwo buckets: the monthly allowance for the current plan (resets at\n``resets_at``) and non-expiring ``extra`` credits from grants and top-ups,\nwhich are spent only once the allowance is gone. ``remaining`` is the sum\nand is what a call is checked against.\n\n``bonus`` is a DEPRECATED alias of ``extra`` (the balance's name until\n2026-09-15), removed on 2026-11-29 with the other deprecated fields on this\nendpoint. It was renamed because the balance holds top-ups you paid for as\nwell as grants.", + "description": "The account's credit balance \u2014 the one pool every capability spends.\n\nTwo buckets: the monthly allowance for the current plan (resets at\n``resets_at``) and non-expiring ``extra`` credits from grants and top-ups,\nwhich are spent only once the allowance is gone. ``remaining`` is the sum\nand is what a call is checked against.\n\n``bonus`` is a DEPRECATED alias of ``extra`` (the balance's name until\n2026-09-15), deprecated with the other deprecated fields on this endpoint and kept\nfor existing callers. It was renamed because the balance holds top-ups you paid for as\nwell as grants.", "properties": { "total": { "default": 0, @@ -2576,7 +2576,7 @@ "type": "object" }, "MeUsageOut": { - "description": "The calling account's credit balance, and what it can still buy.\n\n``credits`` is the balance and ``costs`` is the price list. Those two,\nplus ``cost_options``, are the whole contract for a new integration.\n\nThe ``verify`` / ``ask`` / ``assess`` blocks are DEPRECATED projections of\nthat same pool into each capability's unit, kept until 2026-11-29 for\nclients already reading them.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and carries a\nper-account daily fair-use cap instead; it rejects with 429, not 402.\n\nThe Pro plan's ``plan`` is ``pro`` since 2026-09-15; it was ``developer``\nbefore (#664). ``plan_label`` reads \"Pro\" either way.", + "description": "The calling account's credit balance, and what it can still buy.\n\n``credits`` is the balance and ``costs`` is the price list. Those two,\nplus ``cost_options``, are the whole contract for a new integration.\n\nThe ``verify`` / ``ask`` / ``assess`` blocks are DEPRECATED projections of\nthat same pool into each capability's unit, kept for clients\nalready reading them.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and carries a\nper-account daily fair-use cap instead; it rejects with 429, not 402.\n\nThe Pro plan's ``plan`` is ``pro`` since 2026-09-15; it was ``developer``\nbefore (#664). ``plan_label`` reads \"Pro\" either way.", "properties": { "plan": { "default": "free", @@ -2653,7 +2653,7 @@ "type": "object" }, "MeUsageQuotaOut": { - "description": "DEPRECATED. One capability's share of the pool, projected into its unit.\n\n**Removed 2026-11-29**, with the per-block ``credits`` alias below. Read\n``MeUsageOut.credits`` and ``MeUsageOut.costs`` instead and do the\ndivision \u2014 it is one line, and it is the same one this block does::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nThese are **projections, not allowances**. Every billable capability draws\non the single balance in ``MeUsageOut.credits``, at the weight in\n``MeUsageOut.costs``; this block answers \"how many ``/verify`` calls could\nI still make if I spent everything on them\". Spending on any capability\nmoves every block, and the division floors \u2014 a ``verify`` block whose\nweight is 10 ticks once per 10 credits spent anywhere.\n\nThey predate the pool, when each really was its own allowance, and that is\nthe reason to retire them: two capabilities at the same price emit\nbyte-identical objects (``ask`` and ``assess`` are both 1 credit today),\nso the shape suggests a per-capability allowance that no longer exists.\n\n``bonus`` is ``MeUsageOut.credits.extra``, the non-expiring balance, in\nthis capability's unit.\n``quota_used`` is derived as ``quota_total - quota_remaining``, so\n``used + remaining == total`` always holds.\n\n``credits`` is a DEPRECATED alias of ``bonus``, kept for compatibility\nuntil 2026-11-29. It never meant the pool \u2014 before the pool existed it\nmeant this capability's one-off top-up balance, which is exactly what\n``bonus`` now reports.", + "description": "DEPRECATED. One capability's share of the pool, projected into its unit.\n\n**Deprecated**, with the per-block ``credits`` alias below, and kept for\nexisting callers. Read\n``MeUsageOut.credits`` and ``MeUsageOut.costs`` instead and do the\ndivision \u2014 it is one line, and it is the same one this block does::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nThese are **projections, not allowances**. Every billable capability draws\non the single balance in ``MeUsageOut.credits``, at the weight in\n``MeUsageOut.costs``; this block answers \"how many ``/verify`` calls could\nI still make if I spent everything on them\". Spending on any capability\nmoves every block, and the division floors \u2014 a ``verify`` block whose\nweight is 10 ticks once per 10 credits spent anywhere.\n\nThey predate the pool, when each really was its own allowance, and that is\nthe reason to retire them: two capabilities at the same price emit\nbyte-identical objects (``ask`` and ``assess`` are both 1 credit today),\nso the shape suggests a per-capability allowance that no longer exists.\n\n``bonus`` is ``MeUsageOut.credits.extra``, the non-expiring balance, in\nthis capability's unit.\n``quota_used`` is derived as ``quota_total - quota_remaining``, so\n``used + remaining == total`` always holds.\n\n``credits`` is a DEPRECATED alias of ``bonus``, kept for compatibility\nwith existing callers. It never meant the pool \u2014 before the pool existed it\nmeant this capability's one-off top-up balance, which is exactly what\n``bonus`` now reports.", "properties": { "quota_used": { "default": 0, diff --git a/src/lenz_io/__init__.py b/src/lenz_io/__init__.py index 51c22e2..3fe51ec 100644 --- a/src/lenz_io/__init__.py +++ b/src/lenz_io/__init__.py @@ -18,16 +18,16 @@ # 1. /extract — pull verifiable claims out of text (free, 1000/day) out = client.extract(text=llm_output) - claims = out.identified_claims or [out.claim] + claims = [c.claim for c in out.claims] # 2. /assess — 20 claims a call (extract finds up to 100), one row per claim, same order (paid) quick = [row for i in range(0, len(claims), 20) for row in client.assess(claims=claims[i : i + 20]).claims] - # a row with verdict == "Error" got no verdict: see its error_code and hint; - # a compound item lists the claims it did not assess in identified_claims + # a row with status == "failed" got no verdict: see its failure.code and failure.hint; + # a compound item lists the claims it did not assess in more_claims # 3. /verify — escalate the low-confidence rows to the full pipeline (~90s, paid) # verify_batch_and_wait takes up to 20 claims a call: the first 20 here - doubtful = [{"claim": c.claim} for c in quick if c.verdict != "Error" and c.confidence == "low"][:20] + doubtful = [{"claim": c.claim} for c in quick if c.status == "completed" and c.confidence == "low"][:20] results = client.verify_batch_and_wait(claims=doubtful) if doubtful else [] # 4. /ask — follow-up questions grounded on a verification @@ -74,6 +74,7 @@ AssessClaim, Assessment, AssessResponse, + AssessStatus, Audit, BatchAccepted, BatchItemResult, @@ -89,6 +90,7 @@ EntityRef, Escalation, EscalationPolicy, + ExtractedClaim, ExtractedClaims, ExtractedEntity, ExtractStatus, @@ -144,6 +146,7 @@ "AskReply", "AssessClaim", "AssessResponse", + "AssessStatus", "Assessment", "Audit", "BatchAccepted", @@ -166,6 +169,7 @@ "Escalation", "EscalationPolicy", "ExtractStatus", + "ExtractedClaim", "ExtractedClaims", "ExtractedEntity", "FailureBlock", diff --git a/src/lenz_io/cli/render.py b/src/lenz_io/cli/render.py index 80e7e05..753cfca 100644 --- a/src/lenz_io/cli/render.py +++ b/src/lenz_io/cli/render.py @@ -109,15 +109,12 @@ def render_extract(out: Output, result: ExtractedClaims) -> None: if out.json_mode: out.emit_json(_model_json(result)) return - # The server splits a claim set across two fields: the primary claim lands - # in ``claim`` and any extras in ``identified_claims``. Neither alone is the - # full list — the primary is usually NOT echoed into ``identified_claims`` — - # so render the union so the primary is never dropped (and the count is - # right). Single-claim input → just ``claim``. - primary = (getattr(result, "claim", "") or "").strip() - claims = [primary] if primary else [] - for c in result.identified_claims or []: - c = (c or "").strip() + # ``claims`` is the whole list, from either response shape (the model + # builds it from ``claim`` + ``identified_claims`` when that is what the + # server sent). Blank and repeated entries are dropped. + claims: list[str] = [] + for item in result.claims: + c = (item.claim or "").strip() if c and c not in claims: claims.append(c) if len(claims) > 1: @@ -151,7 +148,7 @@ def _render_positions(out: Output, result: ExtractedClaims, claim: str, *, inden input, where there is nothing to index) and the passage itself. The passage is the user's own text, so it prints with ``markup=False``. """ - for loc in result.locations or []: + for loc in result.claims: if (loc.claim or "").strip() != claim: continue for pos in loc.positions or []: @@ -171,9 +168,10 @@ def render_assess(out: Output, result: AssessResponse) -> None: return for c in claims: color = _VERDICT_COLOR.get(c.verdict, "white") - # An Error row (list form) names its cause in place of the confidence, - # which is meaningless there. - detail = c.error_code if c.verdict == "Error" and c.error_code else c.confidence + # A failed row (list form) names its cause in place of the confidence, + # which is meaningless there. ``error_code`` is the cause in its + # original spelling, from either response shape. + detail = c.error_code if c.status == "failed" and c.error_code else c.confidence out.console.print(f"[{color}]{c.verdict or '?'}[/{color}] ({detail}) — {c.claim}") # The reviewers' notes, as plain text: `markup=False` because the # words are a model's, and a stray "[bold]" in them is not ours. @@ -181,9 +179,9 @@ def render_assess(out: Output, result: AssessResponse) -> None: out.console.print(f" {c.rationale}", markup=False, highlight=False) if c.dissent: out.console.print(f" One reviewer disagreed: {c.dissent}", markup=False, highlight=False) - if c.identified_claims: + if c.more_claims: out.console.print(" [dim]also found:[/dim]") - for other in c.identified_claims: + for other in c.more_claims: out.console.print(f" • {other}") if c.hint: out.console.print(f" [dim]{c.hint}[/dim]") @@ -395,7 +393,7 @@ def render_task_status(out: Output, st: TaskStatus, *, task_id: str = "") -> Non if st.claims: out.console.print("[dim]claims found:[/dim]") for i, claim in enumerate(st.claims, 1): - out.console.print(f" {i}. {claim.text}") + out.console.print(f" {i}. {claim.claim}") ref = task_id or "" # Non-interactive resolution (agents/scripts): `--claim` picks by index # and `--detach` returns the spawned task_id(s) without blocking. Drop diff --git a/src/lenz_io/cli/verify.py b/src/lenz_io/cli/verify.py index 0deb9e3..f0ca5a5 100644 --- a/src/lenz_io/cli/verify.py +++ b/src/lenz_io/cli/verify.py @@ -198,7 +198,7 @@ def _poll( # Sub-claims inherit the parent submission's depth server-side # (/select reads it off the task meta), so nothing to send here. items = client.select(task_id, claims=texts).items - picks = [(it.task_id, it.claim_text or txt) for it, txt in zip(items, texts, strict=True)] + picks = [(it.task_id, it.claim or txt) for it, txt in zip(items, texts, strict=True)] # detach, or >1 claim → batch path; exactly one → keep the # single-verdict flow (nicer than a 1-row table). if detach or len(picks) > 1: @@ -256,7 +256,7 @@ def _resolve_multi_claim(out: Output, task_id: str, st: TaskStatus, selection: l Otherwise json mode emits the needs_input object and exits 3 (never hangs on a prompt that can't happen); a TTY shows the checkbox picker. """ - options = [c.text for c in st.claims] + options = [c.claim for c in st.claims] if selection is not None: return [options[i] for i in _selection_to_indices(selection, len(options))] if out.json_mode: diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index b496e3b..b45141e 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -201,8 +201,13 @@ def _batch_item_body(item: Any) -> Any: An item without ``claim`` is forwarded verbatim, so every existing caller keeps a byte-identical request body. An item with ``claim`` is sent as - ``text`` — the wire key the server has always accepted. + ``text`` — the wire key the server has always accepted. An empty + ``webhook_url`` is left out, as on the batch itself: it has always meant + the key's default, and leaving it out keeps that meaning on every API + version. """ + if isinstance(item, dict) and item.get("webhook_url") == "": + item = {k: v for k, v in item.items() if k != "webhook_url"} if "claim" not in item: return item body = {k: v for k, v in item.items() if k != "claim"} @@ -659,18 +664,18 @@ def extract( one an unfocused call would have returned too, verbatim. When the text has claims but none fall within the focus, ``status`` - is ``"no_match"`` and ``identified_claims`` is empty — the unfocused + is ``"no_match"`` and ``claims`` is empty — the unfocused list is never substituted. Widen the focus and call again. A focused call costs the same single unit of the daily cap. ``locate`` (optional): with ``True``, only the claims that could be - traced directly back to the text are returned, and ``locations`` - says where the text makes each one (``start``/``end`` index the + traced directly back to the text are returned, and each claim's + ``positions`` says where the text makes it (``start``/``end`` index the ``text`` you sent, in code points, so ``text[start:end]`` is the passage). A claim found nowhere in the text, or found with a different figure, is left out; if that leaves no claim, ``status`` is ``"not_a_claim"``. Locating adds a few seconds. If the claims - cannot be located, ``locations`` is ``None`` and the list is returned + cannot be located, ``positions`` is ``None`` and the list is returned unfiltered. Leave it ``None`` to use the server default (currently off); an explicit ``False`` is sent as such. @@ -720,7 +725,7 @@ def assess( ``extract`` in the ladder:: out = client.extract(text=llm_output) - claims = out.identified_claims or [out.claim] + claims = [c.claim for c in out.claims] quick = [row for i in range(0, len(claims), 20) # 20 a call for row in client.assess(claims=claims[i : i + 20]).claims] @@ -733,14 +738,16 @@ def assess( ``verification_url`` pointing at the deep ``Verification`` when /assess found a matching stored claim. - A row with ``verdict == "Error"`` could not be given a verdict. It - stays in position, is not charged, and says why: ``error_code`` is - ``no_claim`` / ``framing_failed`` / ``upstream_unavailable`` / - ``timeout`` (an open set; the last two are worth resending as-is), and - ``hint`` is one sentence on what to send next. A compound list item is assessed on its main - claim; the other claims found in it are listed on that row in - ``identified_claims`` (with a ``hint``) — send them as their own - items to check the rest. ``hint`` is ``None`` on a plain verdict row. + A row with ``status == "failed"`` could not be given a verdict (its + ``verdict`` reads ``"Error"``). It stays in position, is not charged, + and says why: ``failure.code`` is ``no_checkable_claim`` / + ``framing_failed`` / ``upstream_unavailable`` / ``timeout`` (an open + set; the last two are worth resending as-is), and ``failure.hint`` is + one sentence on what to send next. A compound list item is assessed on + its main claim; the other claims found in it are listed on that row in + ``more_claims`` — send them as their own items to check the rest. + (The deprecated ``error_code``, ``hint`` and ``identified_claims`` keep + their original meaning.) Use ``confidence`` to decide when to escalate: ``"low"`` rows are worth re-running through ``verify_batch_and_wait`` for the deep @@ -978,19 +985,21 @@ def verify_batch_and_wait( terminal, timed_out, gone = self._poll_to_terminal(ids, timeout, on_progress) results: list[BatchItemResult] = [] + # Built with ``claim_text`` so ``model_dump()`` keeps the keys it has + # always had; ``BatchItemResult.claim`` reads the same string. for it in accepted.items: # preserve input order status = terminal.get(it.task_id) if it.task_id in gone: # Removed by the account's retention period (410): terminal, # with no status to carry. - results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="failed")) + results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim, status="failed")) elif not it.task_id or it.task_id in timed_out or status is None: - results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="timeout")) + results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim, status="timeout")) elif status.status == "completed" and status.result is not None: results.append( BatchItemResult( task_id=it.task_id, - claim_text=it.claim_text, + claim_text=it.claim, status="completed", verification=status.result, status_detail=status, @@ -998,14 +1007,12 @@ def verify_batch_and_wait( ) elif status.status == "needs_input": results.append( - BatchItemResult( - task_id=it.task_id, claim_text=it.claim_text, status="needs_input", status_detail=status - ) + BatchItemResult(task_id=it.task_id, claim_text=it.claim, status="needs_input", status_detail=status) ) else: # failed, or completed-without-result (treated as failed). results.append( - BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="failed", status_detail=status) + BatchItemResult(task_id=it.task_id, claim_text=it.claim, status="failed", status_detail=status) ) return results @@ -1521,6 +1528,8 @@ def _verification_from_terminal(self, status: TaskStatus, task_id: str) -> Verif ) # failed. Server sends the diagnostic under ``error``; fall back to the # legacy fields for resilience. + # ``failure.detail`` is the sentence in the newer response shape; + # ``error`` carries the original one (and is filled from the block). detail = status.error or status.failure_detail or status.failure_reason or "unknown" if status.retryable: fix = "Transient provider outage — retry the same request after a short wait." @@ -1564,8 +1573,12 @@ def _verify_submit( payload: dict[str, Any] = { "text": claim or text, "source_url": source_url, - "webhook_url": webhook_url, } + # Omit-when-empty: no ``webhook_url`` means the key's default webhook. + # An empty string is never sent, so a request keeps that meaning on + # every API version (a newer one reads ``""`` as "no webhook"). + if webhook_url: + payload["webhook_url"] = webhook_url # Omit-when-empty so existing English callers keep byte-identical # request bodies (no extra "language": "" key). if language: @@ -1598,7 +1611,10 @@ def _verify_batch( # ``VerifyBatchItem`` TypedDict is purely for IDE autocompletion # (revised SDK plan decision 1C — no Pydantic coercion, keep the # runtime contract a plain dict). - payload: dict[str, Any] = {"claims": [_batch_item_body(c) for c in claims], "webhook_url": webhook_url} + payload: dict[str, Any] = {"claims": [_batch_item_body(c) for c in claims]} + # Omit-when-empty, as on ``verify``: no ``webhook_url`` means the key's default. + if webhook_url: + payload["webhook_url"] = webhook_url if language: payload["language"] = language if visibility: diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 07662e4..47c9583 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -192,7 +192,9 @@ class LenzValidationError(LenzError): """422 — request body failed schema validation. ``errors`` is a list of per-field error dicts as returned by Ninja: - ``[{"loc": [...], "msg": "...", "type": "..."}]``. + ``[{"loc": [...], "msg": "...", "type": "..."}]``, read from the body's + ``detail`` list (the original response shape) or its ``errors`` list + (the newer one, where ``detail`` is a sentence and becomes the message). """ errors: list[dict[str, Any]] = [] # noqa: RUF012 — overridden per-instance @@ -202,7 +204,8 @@ class LenzRateLimitError(LenzError): """429 — rate limited. * ``retry_after`` — seconds until the next allowed call, resolved - from the ``Retry-After`` header or the body's ``reset_in_seconds``. + from the ``Retry-After`` header or the body's ``reset_in_seconds`` / + ``retry_after``. * ``limit`` — the cap that was hit, when the server states it. * ``reset_in_seconds`` — the body's raw echo of the same wait. @@ -431,6 +434,10 @@ class LenzWebhookSignatureError(LenzError): # inside a submit would block the caller, most likely into the same answer. NO_RETRY_429_CODES = ("review_in_flight",) +# The 429s that refuse a submit while the account's earlier ones run. They +# stated their wait as ``retry_after_seconds``, never ``reset_in_seconds``. +_IN_FLIGHT_429_CODES = ("review_in_flight", "citecheck_in_flight") + _STATUS_MAP: dict[int, tuple[type[LenzError], str, str]] = { 401: ( LenzAuthError, @@ -561,18 +568,24 @@ def map_response_to_error( if status_code == 409 and isinstance(err, (LenzVerificationNotReadyError, LenzPipelineError)): # The generic 4xx advice ("retry; file an issue") is wrong for both: # the server's own hint says what to do, with a class default behind it. + # A failed run states its cause flat in the original response shape + # and in one ``failure`` block in the newer one; read both, flat first. + raw_failure = parsed.get("failure") + failure: dict[str, Any] = raw_failure if isinstance(raw_failure, dict) else {} err.task_id = _opt_str(parsed.get("task_id")) - err.hint = _opt_str(parsed.get("hint")) + err.hint = _opt_str(parsed.get("hint")) or _opt_str(failure.get("hint")) if isinstance(err, LenzVerificationNotReadyError): err.status = _opt_str(parsed.get("status")) err.fix = err.hint or "Wait for the run with client.wait(task_id), then read its result." else: - err.failure_reason = _opt_str(parsed.get("failure_reason")) - err.failure_class = _opt_str(parsed.get("failure_class")) + reason = _opt_str(parsed.get("failure_reason")) or _opt_str(failure.get("code")) + # The original spelling of "nothing checkable" on a verification. + err.failure_reason = "not_a_claim" if reason == "no_checkable_claim" else reason + err.failure_class = _opt_str(parsed.get("failure_class")) or _opt_str(failure.get("failure_class")) # Only a real boolean is a retry signal, as in the wait path. - retryable = parsed.get("retryable") + retryable = parsed.get("retryable", failure.get("retryable")) err.retryable = retryable if isinstance(retryable, bool) else None - err.doc_url = _opt_str(parsed.get("docs_url")) or err.doc_url + err.doc_url = _opt_str(parsed.get("docs_url")) or _opt_str(failure.get("docs_url")) or err.doc_url if err.hint: err.fix = err.hint elif err.retryable: @@ -609,6 +622,12 @@ def map_response_to_error( # sending null precisely so "unknown" stays distinguishable from # "zero". Collapsing that here would throw the distinction away. err.remaining = _opt_int(parsed.get("remaining")) + if err.remaining is None and "remaining" not in parsed and "requested" not in parsed: + # A body without the capability figure (the newer response shape + # on some endpoints): the pool divided by this one call's price. + pool, price = _opt_int(parsed.get("credits_remaining")), _opt_int(parsed.get("cost")) + if pool is not None and price: + err.remaining = pool // price err.requested = _opt_int(parsed.get("requested")) # The pool behind the capability figure above, and this call's price in # credits. The body's ``credits_remaining`` lands on ``credit_balance``, @@ -630,6 +649,10 @@ def map_response_to_error( elif isinstance(err, LenzRateLimitError): err.limit = _opt_int(parsed.get("limit")) err.reset_in_seconds = _opt_int(parsed.get("reset_in_seconds")) + if err.reset_in_seconds is None and code not in _IN_FLIGHT_429_CODES: + # The newer response shape names every wait ``retry_after``. The + # in-flight refusals never carried ``reset_in_seconds``. + err.reset_in_seconds = _opt_int(parsed.get("retry_after")) rl_upgrade_url = parsed.get("upgrade_url") err.upgrade_url = rl_upgrade_url if isinstance(rl_upgrade_url, str) else "" # Header first, then the body. `reset_in_seconds` is what the server diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 335d52f..d6146d9 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -12,16 +12,68 @@ - ``claim`` : str — the framed claim text - ``verdict`` : str — "True" | "Mostly True" | "Mixed" | "Mostly False" | "False" | "Error" + ("Error": an ``assess`` row with ``status == "failed"``) - ``confidence`` : str — "high" | "medium" | "low" (categorical) - ``lenz_score`` : int | None - 1-10 integer (deep / list; /assess omits) """ from __future__ import annotations -from collections.abc import ItemsView, KeysView, ValuesView +import functools +from collections.abc import Callable, ItemsView, KeysView, ValuesView +from datetime import datetime, timezone from typing import Any, Literal -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PrivateAttr, + SerializerFunctionWrapHandler, + model_serializer, + model_validator, +) + +#: Where a validator lists the attributes it filled in that the response did +#: not carry. ``_Lax`` moves it to a private attribute; ``model_dump()`` leaves +#: those attributes out, so a dump is always the shape the server sent. +_DERIVED_KEY = "__lenz_derived__" + + +def _reads_both(*new_fields: str, always_hide: bool = False) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """Mark what a ``_read_both_shapes`` validator derived, so ``model_dump()`` + shows the shape the server sent. + + ``new_fields`` are the attributes this release added for the newer + response shape. Hidden from a dump: each of them the input did not carry + (so an original-shape response dumps exactly as it did before this + release), and, on a newer-shape input (one carrying any of them, or any + input with ``always_hide``), every attribute the input did not carry. A + value the validator rewrote (a failed row's ``verdict``) dumps as sent. + """ + + def wrap(fn: Callable[..., Any]) -> Callable[..., Any]: + @functools.wraps(fn) + def inner(cls: Any, data: Any) -> Any: + out = fn(cls, data) + if not isinstance(data, dict) or not isinstance(out, dict): + return out + hidden = {k for k in new_fields if k not in data} + if always_hide or any(k in data for k in new_fields): + hidden |= {k for k in out if k not in data} + hidden |= {k for k in cls.model_fields if k not in data} + rewritten = { + k: v + for k, v in data.items() + if k in out and not isinstance(v, (dict, list)) and out[k] is not v and out[k] != v + } + if not hidden and not rewritten: + return out + return {**out, _DERIVED_KEY: {"hidden": sorted(hidden), "sent": rewritten}} + + return inner + + return wrap class _Lax(BaseModel): @@ -35,6 +87,106 @@ class _Lax(BaseModel): model_config = ConfigDict(extra="allow") + # The attributes filled in from the other response shape (see + # ``_reads_both``): readable, never dumped. + _lenz_derived: frozenset[str] = PrivateAttr(default_factory=frozenset) + _lenz_sent: dict[str, Any] = PrivateAttr(default_factory=dict) + + @model_validator(mode="after") + def _collect_derived(self) -> Any: + extra = self.__pydantic_extra__ + if extra and _DERIVED_KEY in extra: + marks = extra.pop(_DERIVED_KEY) + self._lenz_derived = frozenset(marks["hidden"]) + self._lenz_sent = dict(marks["sent"]) + return self + + @model_serializer(mode="wrap") + def _dump_as_sent(self, handler: SerializerFunctionWrapHandler) -> Any: + data = handler(self) + if isinstance(data, dict): + for key in self._lenz_derived: + data.pop(key, None) + for key, value in self._lenz_sent.items(): + if key in data: + data[key] = value + return data + + +# ── Reading both response shapes ───────────────────────────────────────── +# +# The API is gaining a second, dated response shape that renames a handful of +# fields (``claim_text`` -> ``claim``, ``modified_at`` -> ``completed_at``, a +# failed item's ``error`` / ``error_code`` / ``failure_reason`` -> one +# ``failure`` block, ...). This release still asks for the original shape, but +# every model reads either one: the new name is a first-class attribute, and +# every old attribute keeps its old meaning, computed from whichever shape +# arrived. The helpers below are the shared rules. + +#: The one code for "the input holds nothing that can be checked", in the +#: newer response shape. The original shape spells it per endpoint: +#: ``not_a_claim`` (/extract, /verify) and ``no_claim`` (/assess, /review). +NO_CHECKABLE_CLAIM = "no_checkable_claim" +_OLD_NO_CLAIM_CODES = ("not_a_claim", "no_claim") + + +def _new_code(code: Any) -> Any: + """An old per-endpoint spelling of "nothing checkable" -> the one new code.""" + return NO_CHECKABLE_CLAIM if code in _OLD_NO_CLAIM_CODES else code + + +def _old_code(code: Any, old: str) -> Any: + """The new code -> the spelling ``old`` this field always carried.""" + return old if code in (NO_CHECKABLE_CLAIM, *_OLD_NO_CLAIM_CODES) else code + + +def _utc_day(iso: Any) -> Any: + """The UTC calendar day of an ISO-8601 string, or ``None``.""" + if not isinstance(iso, str) or not iso: + return None + try: + when = datetime.fromisoformat(iso.replace("Z", "+00:00")) + except ValueError: + return None + if when.tzinfo is None: + when = when.replace(tzinfo=timezone.utc) + return when.astimezone(timezone.utc).date() + + +def _modified_at_from(created_at: Any, completed_at: Any) -> str | None: + """The original ``modified_at`` rule: the completion time when it falls on a + later UTC calendar day than the creation time, else ``None``.""" + created, completed = _utc_day(created_at), _utc_day(completed_at) + if created is None or completed is None or completed <= created: + return None + return str(completed_at) + + +def _with_completion_times(data: Any) -> Any: + """Fill ``completed_at`` and ``modified_at`` from whichever one arrived. + + ``modified_at`` from ``completed_at``: the original later-calendar-day + rule. ``completed_at`` from ``modified_at``: exact when ``modified_at`` is + set; when it is null (a same-day completion) the original shape does not + say when the verification completed, so ``completed_at`` stays ``None``. + """ + if not isinstance(data, dict): + return data + if "completed_at" in data and "modified_at" not in data: + return {**data, "modified_at": _modified_at_from(data.get("created_at"), data.get("completed_at"))} + if "modified_at" in data and "completed_at" not in data: + return {**data, "completed_at": data.get("modified_at")} + return data + + +def _mirror(data: dict[str, Any], new: str, old: str) -> dict[str, Any]: + """Copy a field across a plain rename, in whichever direction is missing.""" + if new in data and old not in data: + return {**data, old: data[new]} + if old in data and new not in data: + return {**data, new: data[old]} + return data + #: Closed set of failure causes on a ``failed`` verification, mirroring the #: Node SDK's ``FailureClass`` union. Exported for callers who want exhaustive @@ -112,11 +264,23 @@ class Audit(_Lax): class CandidateClaim(_Lax): - """One of multiple distinct claims framing found in the submitted text.""" + """One of multiple distinct claims framing found in the submitted text. - text: str = "" + ``claim`` is the claim's text. ``text`` is its older name, the same + string, kept for existing code (deprecated; either shape fills both). + """ + + claim: str = "" + #: Deprecated: the same string as :attr:`claim`. + text: str = Field(default="", json_schema_extra={"deprecated": True}) domain: str = "" + @model_validator(mode="before") + @classmethod + @_reads_both("claim") + def _read_both_shapes(cls, data: Any) -> Any: + return _mirror(data, "claim", "text") if isinstance(data, dict) else data + class EntityRef(_Lax): """An entity referenced in the claim. @@ -236,8 +400,9 @@ class Verification(_Lax): endpoint, and the webhook payload. The verdict block is FLAT at top level (was nested ``Verdict`` object - pre-unify). ``created_at`` + ``modified_at`` are the only timestamp - fields on the API surface — editorial ``published_at`` is internal-only. + pre-unify). ``created_at`` and ``completed_at`` are its timestamps + (``modified_at`` is the deprecated older one) — editorial + ``published_at`` is internal-only. 1.1.0: dropped ``url`` and ``visibility``. API claims are private by default and referenced by ``verification_id`` only. Cache-hit on @@ -286,7 +451,13 @@ class Verification(_Lax): sources: list[Source] = Field(default_factory=list) audit: Audit = Field(default_factory=Audit) created_at: str | None = None - modified_at: str | None = None + #: When the verification completed. In the original response shape it is + #: known only through ``modified_at``: set when that is, ``None`` on a + #: same-day completion (and on servers that send neither). + completed_at: str | None = None + #: Deprecated: the completion time when it falls on a later UTC calendar + #: day than ``created_at``, else ``None``. Read :attr:`completed_at`. + modified_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) # Output language (ISO 639-1). Always populated by the server when # the SDK is fresh; defaulted to ``'en'`` for resilience against # older cached payloads that lack the field. @@ -301,6 +472,12 @@ class Verification(_Lax): #: reasons why. coverage: Coverage | None = None + @model_validator(mode="before") + @classmethod + @_reads_both("completed_at") + def _read_both_shapes(cls, data: Any) -> Any: + return _with_completion_times(data) + class VerificationListItem(_Lax): """Compact item for the verifications list endpoint and the public @@ -321,10 +498,19 @@ class VerificationListItem(_Lax): # A suggested rewrite of ``claim``. See ``Verification.suggested_rewrite``. suggested_rewrite: str | None = None created_at: str | None = None - modified_at: str | None = None + #: See ``Verification.completed_at``. + completed_at: str | None = None + #: Deprecated. See ``Verification.modified_at``. + modified_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) # Output language (ISO 639-1). See ``Verification.language``. language: str = "en" + @model_validator(mode="before") + @classmethod + @_reads_both("completed_at") + def _read_both_shapes(cls, data: Any) -> Any: + return _with_completion_times(data) + class VerificationList(_Lax): items: list[VerificationListItem] = Field(default_factory=list) @@ -396,7 +582,9 @@ class ClaimLocation(_Lax): #: from lenz_io import ExtractStatus #: #: - ``ready`` — claims were found (and, with a ``focus``, at least one matched). -#: - ``not_a_claim`` — the text holds no verifiable factual claim at all. +#: - ``not_a_claim`` — the text holds no verifiable factual claim at all (the +#: newer response shape spells it ``no_checkable_claim``; this release +#: reads both as ``not_a_claim``). #: - ``no_match`` — claims were found, but none fall within the ``focus``. #: #: A DOCUMENTATION constant, not a guarantee. The model field stays ``str``, @@ -415,27 +603,40 @@ class ClaimLocation(_Lax): ] +class ExtractedClaim(ClaimLocation): + """One claim ``/extract`` found: ``claim``, and with ``locate=True`` its + ``positions`` in the text (``None`` when the call did not locate, or the + claims could not be located).""" + + class ExtractedClaims(_Lax): """Output of ``POST /extract``. + ``claims`` is every claim found, most check-worthy first, always a list + (one entry for one claim, ``[]`` for none). With ``locate=True`` each + entry's ``positions`` says where the text makes it. + ``status`` is one of ``ExtractStatus``: ``ready``, ``not_a_claim``, or — when a ``focus`` was given and no claim fell within it — ``no_match``. - ``no_match`` is a successful answer, not an error: ``identified_claims`` - is empty and the unfocused list is never substituted for it. - - ``locations`` is set only on a call made with ``locate=True``: one - ``ClaimLocation`` per returned claim, in the order of - ``identified_claims`` (one entry for a single ``claim``). It is ``[]`` - when every claim was left out (``status`` is then ``"not_a_claim"``), - and ``None`` when ``locate`` was not set, when the extraction found no - claims, or when the - claims could not be located (the list is then returned unfiltered). - Older servers omit the key; it parses as ``None``. + ``no_match`` is a successful answer, not an error: ``claims`` is empty + and the unfocused list is never substituted for it. In this release + ``status`` keeps the ``not_a_claim`` spelling whichever response shape + arrived (the newer shape spells it ``no_checkable_claim``). + + Deprecated, kept with their original meaning: ``claim`` is the first + claim (``""`` for none), ``identified_claims`` the whole list when there + are two or more (``[]`` for one), and ``locations`` the ``ClaimLocation`` + list of a located call (``None`` when ``locate`` was not set, when the + extraction found no claims, or when the claims could not be located). + Read ``claims`` instead: it is filled from either response shape. """ status: str = "" - claim: str = "" - identified_claims: list[str] = Field(default_factory=list) + claims: list[ExtractedClaim] = Field(default_factory=list) + #: Deprecated: the first of :attr:`claims`. + claim: str = Field(default="", json_schema_extra={"deprecated": True}) + #: Deprecated: :attr:`claims` when there are two or more, else ``[]``. + identified_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) # Deprecated: always empty since 2026-09-12. Kept because the server # still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) @@ -443,7 +644,84 @@ class ExtractedClaims(_Lax): key_entities: list[ExtractedEntity] = Field(default_factory=list) presumed_intent: str = "" original_input: str = "" - locations: list[ClaimLocation] | None = None + #: Deprecated: the ``positions`` of :attr:`claims`, as one + #: ``ClaimLocation`` per claim. + locations: list[ClaimLocation] | None = Field(default=None, json_schema_extra={"deprecated": True}) + + @model_validator(mode="before") + @classmethod + @_reads_both("claims") + def _read_both_shapes(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + out = dict(data) + if out.get("status") == NO_CHECKABLE_CLAIM: + out["status"] = "not_a_claim" + claims = out.get("claims") + if isinstance(claims, list): + # The newer shape: derive the three original fields. + items = [c for c in claims if isinstance(c, dict)] + names = [str(c.get("claim") or "") for c in items] + out.setdefault("claim", names[0] if names else "") + out.setdefault("identified_claims", names if len(names) > 1 else []) + if "locations" not in out: + located = bool(items) and all(c.get("positions") is not None for c in items) + out["locations"] = ( + [{"claim": c.get("claim") or "", "positions": c.get("positions")} for c in items] + if located + else None + ) + return out + # The original shape: build ``claims`` from ``claim`` + ``identified_claims``. + identified = [c for c in out.get("identified_claims") or [] if isinstance(c, str)] + primary = out.get("claim") if isinstance(out.get("claim"), str) else "" + # ``identified_claims`` normally starts with ``claim``; an older server + # sent the extras only, so take the union, ``claim`` first. + names = ([primary] if primary else []) + [c for c in identified if c != primary] + positions: dict[str, Any] = {} + for loc in out.get("locations") or []: + if isinstance(loc, dict) and isinstance(loc.get("claim"), str): + positions.setdefault(loc["claim"], loc.get("positions")) + out["claims"] = [{"claim": n, "positions": positions.get(n)} for n in names] + return out + + +class FailureBlock(_Lax): + """Why something failed: a review, one claim's work inside it, a citation + check, an ``assess`` row or a verification. + + ``code`` is the specific cause (an open set, e.g. ``no_checkable_claim``, + ``insufficient_credits``, ``timeout``), ``detail`` one sentence on what + happened (``None`` from the original response shape, which has no such + sentence here), ``failure_class`` the closed :data:`FailureClass`, + ``retryable`` whether resending the same request can help, ``hint`` one + sentence on what to do next and ``docs_url`` where the class is explained. + + ``failure_reason`` is the older name of ``code`` (deprecated), with its + original spelling: ``no_claim`` where ``code`` reads + ``no_checkable_claim``. Either response shape fills both. + """ + + code: str | None = "" + detail: str | None = None + #: Deprecated: :attr:`code`, spelling "nothing checkable" ``no_claim``. + failure_reason: str | None = Field(default="", json_schema_extra={"deprecated": True}) + failure_class: str | None = "" + retryable: bool | None = None + hint: str | None = None + docs_url: str | None = "" + + @model_validator(mode="before") + @classmethod + @_reads_both("code", "detail") + def _read_both_shapes(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + if "code" in data and "failure_reason" not in data: + return {**data, "failure_reason": _old_code(data["code"], "no_claim")} + if "failure_reason" in data and "code" not in data: + return {**data, "code": _new_code(data["failure_reason"])} + return data class AssessClaim(_Lax): @@ -454,29 +732,38 @@ class AssessClaim(_Lax): ``ClaimDetailOut`` payload at ``GET /api/v1/verifications/{id}`` for callers that want citations and the full audit trail. - A row with ``verdict == "Error"`` could not be given a verdict. On a - list call it stays in position (one row per item sent), is not charged, - and says why: ``error_code`` names the cause and ``hint`` is one - sentence on what to send next. ``hint`` is the field to surface to a - human — it is written per cause and stays correct as causes are added. - A vague input is assessed on its most likely reading, which ``claim`` - carries. A compound input is assessed on its main claim; the other - claims found in it are listed in ``identified_claims`` (also with a - ``hint``). These fields default empty so older servers that don't send - them still parse. + ``status`` is ``"completed"`` for a row with a verdict and ``"failed"`` + for one that could not be given a verdict. A failed row stays in + position (one row per item sent on a list call), is not charged, and + says why in ``failure``: ``failure.code`` names the cause and + ``failure.hint`` is one sentence on what to send next — the field to + surface to a human, written per cause. A vague input is assessed on its + most likely reading, which ``claim`` carries. A compound input is + assessed on its main claim; the other claims found in it are listed in + ``more_claims``. + + The original fields keep their meaning whichever response shape + arrived: a failed row reads ``verdict == "Error"`` and + ``confidence == "low"``, its cause is in ``error_code`` (``no_claim`` + where ``failure.code`` reads ``no_checkable_claim``) and its next step in + ``hint``; ``identified_claims`` is ``more_claims``. These fields default + empty so older servers that don't send them still parse. """ claim: str = "" # Output language (ISO 639-1). Echoes the language requested on the # call, or ``'en'`` when unspecified. Verdict enums always English. language: str = "en" + #: ``"completed"`` (a verdict) or ``"failed"`` (none; see ``failure``). + #: ``""`` only from a server that sent neither a status nor a verdict. + status: str = "" verdict: str = "" # "True" | "Mostly True" | "Mixed" | "Mostly False" | "False" | "Error" confidence: str = "low" # "high" | "medium" | "low" verification_url: str | None = None # ``rationale`` is the reasoning of a reviewer who agrees with the panel's # verdict; ``dissent``, when set, is the reasoning of the reviewer farthest # from it. Both are reviewers' notes, not checked sources; for sourced - # evidence, call ``verify``. Read both as optional: an ``"Error"`` row has + # evidence, call ``verify``. Read both as optional: a failed row has # neither, and a response replayed from before the API added them carries # neither key. rationale: str | None = None @@ -489,28 +776,75 @@ class AssessClaim(_Lax): # check's reasoning and not itself verified: review it, or run it through # ``verify``, before using it. suggested_rewrite: str | None = None - # Only on ``verdict == "Error"`` rows: 'no_claim' | 'framing_failed' | - # 'upstream_unavailable' | 'timeout'. - # - # An OPEN set, deliberately typed ``str`` rather than a Literal: the API - # may add a cause in a minor version, so branch on the ones you know and - # fall through on the rest. - # - # Which are worth resending as-is: 'upstream_unavailable' (a provider was - # down) and 'timeout' (the call ran out of its time budget before this - # item was done — send fewer items per call to make it less likely). - # 'framing_failed' is deterministic, so retrying the same text will not - # help (a provider outage comes back as 'upstream_unavailable' instead). - # 'no_claim' wants a different input; read ``hint``. - error_code: str | None = None + #: Why a failed row has no verdict; ``None`` on a completed row. + #: ``failure.code`` is an OPEN set (``no_checkable_claim`` | + #: ``framing_failed`` | ``upstream_unavailable`` | ``timeout`` today): + #: branch on the ones you know and fall through on the rest. + #: + #: Which are worth resending as-is: ``upstream_unavailable`` (a provider + #: was down) and ``timeout`` (the call ran out of its time budget before + #: this item was done — send fewer items per call to make it less likely). + #: ``framing_failed`` is deterministic, so retrying the same text will not + #: help. ``no_checkable_claim`` wants a different input; read + #: ``failure.hint``. + failure: FailureBlock | None = None + #: Other claims found in the input that were NOT assessed; else empty. + more_claims: list[str] = Field(default_factory=list) + #: Deprecated: ``failure.code`` on a failed row, with "nothing + #: checkable" spelled ``no_claim``; ``None`` on a completed row. + error_code: str | None = Field(default=None, json_schema_extra={"deprecated": True}) # Deprecated: always empty since 2026-09-12, when the ``ambiguous`` cause # that filled it was retired. Kept because the server still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) - # Other claims found in the input that were NOT assessed; else empty. - identified_claims: list[str] = Field(default_factory=list) - # One sentence on what to send next. Set on every Error row and on a row - # with a non-empty ``identified_claims``; ``None`` on a plain verdict row. - hint: str | None = None + #: Deprecated: :attr:`more_claims`. + identified_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) + #: Deprecated: one sentence on what to send next. On a failed row it is + #: ``failure.hint``. The original response shape also set it on a row + #: with other claims found; the newer shape does not. + hint: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + + @model_validator(mode="before") + @classmethod + @_reads_both("status", "failure", "more_claims") + def _read_both_shapes(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + out = _mirror(dict(data), "more_claims", "identified_claims") + status = out.get("status") + verdict = out.get("verdict") + if not status: + status = "failed" if verdict == "Error" else ("completed" if verdict else "") + out["status"] = status + if status == "failed": + # The original meaning of a row with no verdict. + if not verdict: + out["verdict"] = "Error" + if out.get("confidence") is None: + out["confidence"] = "low" + else: + if verdict is None: + out["verdict"] = "" + if out.get("confidence") is None: + out["confidence"] = "low" + failure = out.get("failure") + if isinstance(failure, dict): + code = failure.get("code", failure.get("failure_reason")) + out.setdefault("error_code", _old_code(code, "no_claim")) + out.setdefault("hint", failure.get("hint")) + elif failure is None and status == "failed" and "failure" not in data: + # The original shape: build the block from the row's own fields. + out["failure"] = {"code": _new_code(out.get("error_code") or ""), "hint": out.get("hint")} + return out + + +#: The ``AssessResponse.status`` values this release knows about. A +#: documentation constant, like ``ExtractStatus``: the field stays ``str``. +#: +#: - ``ok`` — at least one row has a verdict (a list may mix in failed rows). +#: - ``no_checkable_claim`` — the input, or every item, holds nothing that +#: can be checked. +#: - ``error`` — no row has a verdict, for another reason (see the rows). +AssessStatus = Literal["ok", "no_checkable_claim", "error"] class AssessResponse(_Lax): @@ -520,23 +854,32 @@ class AssessResponse(_Lax): found in the input — up to 20, at 1 credit each. A text that makes more claims than one call checks gets its most check-worthy 20 checked and the rest listed in ``more_claims``, unchecked and free: send them back with - ``assess(claims=...)``, 20 a call, to check them. ``error`` is set when - the input holds no checkable claim. + ``assess(claims=...)``, 20 a call, to check them. When the input holds no + checkable claim, ``claims`` is empty, ``status`` is + ``"no_checkable_claim"`` and ``failure`` says why. List form (``assess(claims=[...])``): exactly one entry per item sent, - in the order sent. An item that could not be given a verdict is an - ``"Error"`` row in position (see ``AssessClaim``), never a missing one; - the top-level ``error`` stays ``None``. - - When ``claims`` is empty (single form), ``error_code`` is ``'no_claim'``: - the input holds no checkable claim (a vague input is assessed on its - most likely reading instead). It defaults empty, so older servers that - don't send it degrade to the plain ``error`` message. + in the order sent. An item that could not be given a verdict is a + ``"failed"`` row in position (see ``AssessClaim``), never a missing one; + the top-level ``failure`` stays ``None``. + + ``status`` (see ``AssessStatus``) sums the call up: ``ok``, + ``no_checkable_claim`` or ``error``. Computed from the rows when a server + does not send it. + + Deprecated, kept with their original meaning: ``error`` is the sentence + (``failure.detail``) and ``error_code`` is ``'no_claim'`` when the + single form found no checkable claim; both are empty otherwise. """ + status: str = "" claims: list[AssessClaim] = Field(default_factory=list) - error: str | None = None - error_code: str = "" # '' | 'no_claim' + #: Why the single form has no rows; ``None`` otherwise. + failure: FailureBlock | None = None + #: Deprecated: ``failure.detail``. + error: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + #: Deprecated: ``'no_claim'`` when ``failure`` says nothing was checkable. + error_code: str = Field(default="", json_schema_extra={"deprecated": True}) # '' | 'no_claim' # Deprecated: always empty since 2026-09-12. Kept because the server # still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) @@ -544,12 +887,65 @@ class AssessResponse(_Lax): # first; ``[]`` otherwise, on the list form, and from older servers. more_claims: list[str] = Field(default_factory=list) + @model_validator(mode="before") + @classmethod + @_reads_both("status", "failure") + def _read_both_shapes(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + out = dict(data) + failure = out.get("failure") + if isinstance(failure, dict): + code = failure.get("code", failure.get("failure_reason")) + out.setdefault("error", failure.get("detail")) + out.setdefault("error_code", _old_code(code, "no_claim") or "") + elif failure is None and "failure" not in data and (out.get("error") or out.get("error_code")): + out["failure"] = {"code": _new_code(out.get("error_code") or ""), "detail": out.get("error")} + if out.get("error_code") is None: + out["error_code"] = "" + if not out.get("status"): + out["status"] = _assess_status(out) + return out + + +def _assess_status(body: dict[str, Any]) -> str: + """``AssessResponse.status`` for a body that does not carry it.""" + rows = [r for r in body.get("claims") or [] if isinstance(r, dict)] + failed = [r for r in rows if r.get("status") == "failed" or r.get("verdict") in ("Error", None, "")] + if rows and len(failed) < len(rows): + return "ok" + + def _code(block: Any, fallback: Any) -> Any: + if isinstance(block, dict): + return block.get("code", block.get("failure_reason")) + return fallback + + if not rows: + code = _code(body.get("failure"), body.get("error_code")) + return NO_CHECKABLE_CLAIM if _new_code(code) == NO_CHECKABLE_CLAIM else "error" + codes = {_new_code(_code(r.get("failure"), r.get("error_code"))) for r in rows} + return NO_CHECKABLE_CLAIM if codes == {NO_CHECKABLE_CLAIM} else "error" + class TaskAccepted(_Lax): - """Returned by ``POST /verify`` and per item of ``POST /verify/batch``.""" + """Returned by ``POST /verify`` and per item of ``POST /verify/batch`` + and ``POST /verify/{task_id}/select``. + + ``claim`` is the item's claim on a batch or select receipt (``""`` on a + single ``verify`` receipt). ``claim_text`` is its older name, the same + string (deprecated; either response shape fills both). + """ task_id: str = "" - claim_text: str = "" + claim: str = "" + #: Deprecated: the same string as :attr:`claim`. + claim_text: str = Field(default="", json_schema_extra={"deprecated": True}) + + @model_validator(mode="before") + @classmethod + @_reads_both("claim") + def _read_both_shapes(cls, data: Any) -> Any: + return _mirror(data, "claim", "claim_text") if isinstance(data, dict) else data class BatchAccepted(_Lax): @@ -643,14 +1039,23 @@ class TaskStatus(_Lax): # the JSON schema only, so reading them does not warn. candidates: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) similar_claims: list[SimilarVerification] = Field(default_factory=list, json_schema_extra={"deprecated": True}) - # failure branches. The server's failed response is - # ``{"status": "failed", "error": "..."}`` — ``error`` is the live wire - # field. ``failure_reason`` / ``failure_detail`` are kept for forward/back - # compatibility and other channels; read precedence is - # ``error or failure_detail or failure_reason``. - error: str = "" - failure_reason: str = "" - failure_detail: str = "" + #: On ``failed``: why. ``failure.code`` is the specific cause (an open + #: set, e.g. ``research_empty``, ``no_checkable_claim``), ``failure.detail`` + #: one sentence on what happened, ``failure.failure_class`` the closed + #: :data:`FailureClass`, ``failure.retryable`` whether resending the same + #: claim can help, ``failure.hint`` what to send instead (when the server + #: has one) and ``failure.docs_url`` where the class is explained. Filled + #: from either response shape; ``None`` on every other status. + failure: FailureBlock | None = None + # The original failure fields, deprecated and kept with their original + # meaning whichever response shape arrived: ``error`` is the sentence, + # ``failure_reason`` the cause (``not_a_claim`` where ``failure.code`` + # reads ``no_checkable_claim``), and ``failure_class`` / ``retryable`` / + # ``docs_url`` / ``hint`` the same values as on ``failure``. + # ``failure_detail`` is never sent by the API; it stays for old code. + error: str = Field(default="", json_schema_extra={"deprecated": True}) + failure_reason: str = Field(default="", json_schema_extra={"deprecated": True}) + failure_detail: str = Field(default="", json_schema_extra={"deprecated": True}) # WHY it failed — the closed set is ``FailureClass`` (import it for # exhaustive matching). The annotation stays ``str`` on purpose: a # ``Literal`` here would make an unknown class the server adds later a @@ -662,6 +1067,43 @@ class TaskStatus(_Lax): # Where this ``failure_class`` is explained. ``""`` from older servers. docs_url: str = "" + @model_validator(mode="before") + @classmethod + @_reads_both("failure") + def _read_both_shapes(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + out = dict(data) + failure = out.get("failure") + if isinstance(failure, dict): + # The newer shape: the original top-level fields from the block. + code = failure.get("code", failure.get("failure_reason")) + for key, value in ( + ("error", failure.get("detail")), + ("failure_reason", _old_code(code, "not_a_claim")), + ("failure_class", failure.get("failure_class")), + ("retryable", failure.get("retryable")), + ("docs_url", failure.get("docs_url")), + ("hint", failure.get("hint")), + ): + if out.get(key) is None: + out[key] = value + elif out.get("status") == "failed" and failure is None: + # The original shape: the block from the top-level fields. + out["failure"] = { + "code": _new_code(out.get("failure_reason") or ""), + "detail": out.get("error") or out.get("failure_detail") or None, + "failure_class": out.get("failure_class") or "", + "retryable": out.get("retryable") if isinstance(out.get("retryable"), bool) else None, + "hint": out.get("hint") or None, + "docs_url": out.get("docs_url") or "", + } + # ``None`` reads as the field's empty value, never as a validation error. + for key in ("error", "failure_reason", "failure_detail", "failure_class", "docs_url", "hint", "reason"): + if key in out and out[key] is None: + out[key] = "" + return out + class BatchItemResult(_Lax): """Per-item outcome from :meth:`Lenz.verify_batch_and_wait`. @@ -680,11 +1122,19 @@ class BatchItemResult(_Lax): """ task_id: str = "" - claim_text: str = "" + claim: str = "" + #: Deprecated: the same string as :attr:`claim`. + claim_text: str = Field(default="", json_schema_extra={"deprecated": True}) status: Literal["completed", "needs_input", "failed", "timeout"] verification: Verification | None = None status_detail: TaskStatus | None = None + @model_validator(mode="before") + @classmethod + @_reads_both("claim") + def _read_both_names(cls, data: Any) -> Any: + return _mirror(data, "claim", "claim_text") if isinstance(data, dict) else data + class UsageCredits(_Lax): """The account's credit balance — the one pool every capability spends. @@ -696,7 +1146,7 @@ class UsageCredits(_Lax): resets at ``resets_at``, and non-expiring ``extra`` credits from grants and top-ups, spent only once the allowance is gone. ``remaining`` covers both and is what a call is checked against. ``bonus`` is the deprecated old - name of ``extra``, the same number, removed on 2026-11-29. + name of ``extra``, the same number, kept for existing code. Servers predating the credit pool (before 2026-08-29) don't send this block at all, and it then reads as all-zero — check ``usage.credits.total`` @@ -709,14 +1159,14 @@ class UsageCredits(_Lax): #: The non-expiring part of ``remaining``: credits from grants and top-ups, #: spent only once the monthly allowance is gone. extra: int = 0 - #: **Deprecated** old name of :attr:`extra`, the same number, removed on - #: 2026-11-29. Reading it emits a ``DeprecationWarning``; it stays in - #: ``model_dump()`` output (unwarned) for as long as the server sends it. + #: **Deprecated** old name of :attr:`extra`, the same number, kept for + #: existing code. Reading it emits a ``DeprecationWarning``; it stays in + #: ``model_dump()`` output (unwarned), filled from ``extra`` when the + #: server does not send it. bonus: int = Field( default=0, deprecated=( - "UsageCredits.bonus is deprecated and will be removed on 2026-11-29; " - "use `extra` — the same number, the non-expiring part of the balance." + "UsageCredits.bonus is deprecated; use `extra` — the same number, the non-expiring part of the balance." ), ) resets_at: str | None = None @@ -727,8 +1177,8 @@ def _mirror_extra_and_bonus(cls, data: Any) -> Any: """Keep ``extra`` and its deprecated old name in step, in both directions. A server that has not started sending ``extra`` sends only ``bonus``; - a server after the 2026-11-29 removal sends only ``extra``. Mirroring - here means both attributes read correctly either way. + the newer response shape sends only ``extra``. Mirroring here means + both attributes read correctly either way. """ if not isinstance(data, dict): return data @@ -743,9 +1193,11 @@ def _mirror_extra_and_bonus(cls, data: Any) -> Any: class UsageCapacity(_Lax): """DEPRECATED. One capability's share of the pool, in that capability's unit. - **Removed 2026-11-29**, together with the per-block ``credits`` alias. - Read :attr:`Usage.credits` and :attr:`Usage.costs` and do the division — - it is the same one this block does:: + Deprecated, together with the per-block ``credits`` alias, and kept for + existing code: the newer response shape does not send these blocks, and + the SDK then computes them from :attr:`Usage.credits` and + :attr:`Usage.costs`. Read those two and do the division yourself — it is + the same one this block does:: remaining = u.credits.remaining // u.costs[capability] total = u.credits.total // u.costs[capability] @@ -778,7 +1230,7 @@ class UsageCapacity(_Lax): quota_total: int = 0 quota_remaining: int = 0 bonus: int = 0 - #: **Deprecated** alias of :attr:`bonus`, removed on 2026-11-29. It never + #: **Deprecated** alias of :attr:`bonus`, kept for existing code. It never #: meant the credit pool — before the pool existed it meant this #: capability's one-off top-up balance, which is exactly what ``bonus`` #: reports. Reading it emits a ``DeprecationWarning``; it stays in @@ -786,7 +1238,7 @@ class UsageCapacity(_Lax): credits: int = Field( default=0, deprecated=( - "UsageCapacity.credits is deprecated and will be removed on 2026-11-29; " + "UsageCapacity.credits is deprecated; " "use `bonus` — the same number, this capability's non-expiring top-up " "balance. The credit pool itself is `Usage.credits`." ), @@ -798,8 +1250,8 @@ class UsageCapacity(_Lax): def _mirror_bonus_and_credits(cls, data: Any) -> Any: """Keep ``bonus`` and its deprecated alias in step, in both directions. - A server predating the credit pool sends only ``credits``; a server - after the 2026-11-29 removal sends only ``bonus``. Mirroring here means + A server predating the credit pool sends only ``credits``; a block the + SDK computed has only ``bonus``. Mirroring here means both attributes read correctly either way, so the SDK never depends on which side of an API deploy it is talking to. """ @@ -850,7 +1302,9 @@ class Usage(_Lax): #: comparing against it will break on a rename that ought to be free. #: Empty on servers predating this field — fall back to :attr:`plan`. plan_label: str = "" - quota_resets_at: str | None = None + #: Deprecated: the same time as ``credits.resets_at``, which it is + #: filled from when the server does not send it. + quota_resets_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) #: The credit balance — the authoritative number. Empty on older servers. credits: UsageCredits = Field(default_factory=UsageCredits) #: Credits per call, keyed by CAPABILITY, at its default price: @@ -886,14 +1340,17 @@ class Usage(_Lax): #: read ``standard`` on a ``low`` request — the echo describes the #: evidence, the charge follows the request. cost_options: dict[str, dict[str, dict[str, int]]] = Field(default_factory=dict) - #: DEPRECATED — removed 2026-11-29. Derive from :attr:`credits` and + #: DEPRECATED, kept for existing code. Derive from :attr:`credits` and #: :attr:`costs` instead:: #: #: left = u.credits.remaining // u.costs["verify"] + #: + #: Computed that way when the server does not send the block (the newer + #: response shape does not). verify: UsageCapacity = Field(default_factory=UsageCapacity) - #: DEPRECATED — removed 2026-11-29. See :attr:`verify`. + #: DEPRECATED, kept for existing code. See :attr:`verify`. ask: UsageCapacity = Field(default_factory=UsageCapacity) - #: DEPRECATED — removed 2026-11-29. See :attr:`verify`. + #: DEPRECATED, kept for existing code. See :attr:`verify`. assess: UsageCapacity = Field(default_factory=UsageCapacity) extract: UsageExtract = Field(default_factory=UsageExtract) # Whether this key has a webhook signing secret provisioned. ``POST /verify`` @@ -903,6 +1360,49 @@ class Usage(_Lax): # Defaults to ``False`` on servers predating this field. has_webhook_secret: bool = False + @model_validator(mode="before") + @classmethod + @_reads_both(always_hide=True) + def _read_both_shapes(cls, data: Any) -> Any: + """Compute the deprecated blocks the newer response shape leaves out. + + Each per-capability block is the credit pool divided by that + capability's price, exactly as the server computed it; it is filled + only when the server sent the pool and the price but not the block. + """ + if not isinstance(data, dict): + return data + credits, costs = data.get("credits"), data.get("costs") + if not isinstance(credits, dict): + return data + out = dict(data) + if "quota_resets_at" not in out: + out["quota_resets_at"] = credits.get("resets_at") + if not isinstance(costs, dict): + return out + + def _int(value: Any) -> int: + return value if isinstance(value, int) and not isinstance(value, bool) else 0 + + total, remaining, extra = ( + _int(credits.get("total")), + _int(credits.get("remaining")), + _int(credits.get("extra", credits.get("bonus"))), + ) + for capability in ("verify", "ask", "assess"): + cost = _int(costs.get(capability)) + if capability in out or cost <= 0: + continue + quota_total, left = total // cost, remaining // cost + out[capability] = { + "quota_used": quota_total - left, + "quota_total": quota_total, + "quota_remaining": left, + "bonus": extra // cost, + "remaining": left, + } + return out + class AskMessage(_Lax): """One message in an ``/ask`` conversation thread.""" @@ -970,24 +1470,6 @@ class ReviewStarted(_Lax): status: str = "queued" -class FailureBlock(_Lax): - """Why a review, or one claim's work inside it, failed. - - The same fields a failed ``GET /verify/status`` carries: ``failure_reason`` - is the specific cause (an open set, e.g. ``no_claim``, - ``insufficient_credits``, ``timeout``), ``failure_class`` the closed - :data:`FailureClass`, ``retryable`` whether resending the same request can - help, ``hint`` one sentence on what to do next and ``docs_url`` where the - class is explained. - """ - - failure_reason: str | None = "" - failure_class: str | None = "" - retryable: bool | None = None - hint: str | None = None - docs_url: str | None = "" - - class EscalationPolicy(_Lax): """Which quick-checked claims get a deep check, as the review RESOLVED it. @@ -1055,11 +1537,19 @@ class ReviewSummary(_Lax): The ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review that did not ask for the citation check.""" + #: How many claims the draft holds. ``None`` until the draft is read, + #: and from the original response shape, which does not send it. + claims_found: int | None = None claims_selected: int | None = None #: The resolved ``max_assessments``. claim_limit: int = 20 - #: The draft held at least ``claim_limit`` claims: more MAY exist. - claim_limit_reached: bool | None = None + #: Some of the draft's claims were left out (more than ``claim_limit``): + #: they are in ``more_claims``. From the original response shape it is + #: read off ``more_claims`` once the draft is read. + claim_limit_exceeded: bool | None = None + #: Deprecated: the draft held at least ``claim_limit`` claims, so more + #: MAY exist. Read :attr:`claim_limit_exceeded`, which says whether they do. + claim_limit_reached: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) #: The text was cut at 50,000 characters. input_truncated: bool = False assessments: ReviewAssessmentCounts | None = None @@ -1071,8 +1561,10 @@ class ReviewSummary(_Lax): citations_selected: int | None = None #: The resolved ``max_citations``. citation_limit: int | None = None - #: ``citations_found`` is over ``citation_limit``. - citation_limit_reached: bool | None = None + #: ``citations_found`` is over ``citation_limit``: some were left out. + citation_limit_exceeded: bool | None = None + #: Deprecated: the same value as :attr:`citation_limit_exceeded`. + citation_limit_reached: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) citation_checks: ReviewCitationCheckCounts | None = None #: ``len(citation_issues)``. citation_issues: int = 0 @@ -1081,6 +1573,18 @@ class ReviewSummary(_Lax): #: An open set. citations_skipped: str | None = None + @model_validator(mode="before") + @classmethod + @_reads_both("claims_found", "claim_limit_exceeded", "citation_limit_exceeded") + def _read_both_shapes(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + out = _mirror(dict(data), "citation_limit_exceeded", "citation_limit_reached") + if "claim_limit_reached" not in out and "claims_found" in out: + found, limit = out.get("claims_found"), out.get("claim_limit", 20) + out["claim_limit_reached"] = found >= limit if isinstance(found, int) and isinstance(limit, int) else None + return out + class ReviewCredits(_Lax): """``charged`` is the net credits the review cost the account, its @@ -1104,8 +1608,15 @@ class ReviewResult(_Lax): class ReviewAssessment(_Lax): """A claim's quick check. ``status``: ``pending`` | ``running`` | ``completed`` | ``failed``. On ``completed`` the fields mean what they - mean on an ``/assess`` row; on ``failed`` see ``error_code`` (an open - set, as on ``/assess``), ``hint`` and ``failure``.""" + mean on an ``/assess`` row; on ``failed`` see ``failure`` (its ``code`` + an open set, as on ``/assess``). ``more_claims`` lists other claims + found in the passage that were not checked. + + Deprecated, kept with their original meaning: ``error_code`` is + ``failure.code`` on a failed row (``no_claim`` where the code reads + ``no_checkable_claim``), ``identified_claims`` is ``more_claims``, and + ``hint`` is the row's own hint from the original response shape + (``None`` from the newer one; read ``failure.hint``).""" status: str = "" verdict: str | None = None @@ -1113,9 +1624,13 @@ class ReviewAssessment(_Lax): rationale: str | None = None dissent: str | None = None verification_url: str | None = None - error_code: str | None = None - identified_claims: list[str] = Field(default_factory=list) - hint: str | None = None + more_claims: list[str] = Field(default_factory=list) + #: Deprecated: ``failure.code`` on a failed row. + error_code: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + #: Deprecated: :attr:`more_claims`. + identified_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) + #: Deprecated. See the class docstring. + hint: str | None = Field(default=None, json_schema_extra={"deprecated": True}) #: With ``suggest_edits=True``: the claim with its wrong part corrected, #: from the quick check's reasoning, when it found the claim "False" or #: "Mostly False" with high confidence. ``None`` otherwise, and from @@ -1124,6 +1639,18 @@ class ReviewAssessment(_Lax): suggested_rewrite: str | None = None failure: FailureBlock | None = None + @model_validator(mode="before") + @classmethod + @_reads_both("more_claims") + def _read_both_shapes(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + out = _mirror(dict(data), "more_claims", "identified_claims") + failure = out.get("failure") + if "error_code" not in out and isinstance(failure, dict): + out["error_code"] = _old_code(failure.get("code", failure.get("failure_reason")), "no_claim") + return out + class ReviewEntity(_Lax): """An entity the deep check found in the claim. ``qid`` is its Wikidata @@ -1164,11 +1691,20 @@ class ReviewVerification(_Lax): suggested_rewrite: str | None = None warnings: list[str] = Field(default_factory=list) created_at: str | None = None - modified_at: str | None = None + #: See ``Verification.completed_at``. + completed_at: str | None = None + #: Deprecated. See ``Verification.modified_at``. + modified_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) verification_url: str | None = None url: str | None = None failure: FailureBlock | None = None + @model_validator(mode="before") + @classmethod + @_reads_both("completed_at") + def _read_both_shapes(cls, data: Any) -> Any: + return _with_completion_times(data) + class SuggestedEdit(_Lax): """One replacement in the draft: the span ``start``..``end`` of the @@ -1469,10 +2005,19 @@ class CitecheckSummary(_Lax): citations_found: int | None = None citations_selected: int | None = None citation_limit: int | None = None - citation_limit_reached: bool | None = None + #: ``citations_found`` is over ``citation_limit``: some were left out. + citation_limit_exceeded: bool | None = None + #: Deprecated: the same value as :attr:`citation_limit_exceeded`. + citation_limit_reached: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) citation_checks: ReviewCitationCheckCounts | None = None citation_issues: int = 0 + @model_validator(mode="before") + @classmethod + @_reads_both("citation_limit_exceeded") + def _read_both_shapes(cls, data: Any) -> Any: + return _mirror(data, "citation_limit_exceeded", "citation_limit_reached") if isinstance(data, dict) else data + class Citecheck(_Lax): """``GET /citechecks/{citecheck_id}``: a citation check as it stands. @@ -1548,6 +2093,15 @@ class ReviewEnvelope(_Lax): more_citations: list[ReviewMoreCitation] | None = None failure: FailureBlock | None = None + @model_validator(mode="after") + def _claim_limit_exceeded_from_more_claims(self) -> ReviewEnvelope: + """The original shape says only that the limit was REACHED; whether + claims were left out is whether ``more_claims`` lists any.""" + summary = self.summary + if summary.claim_limit_exceeded is None and self.more_claims is not None: + summary.claim_limit_exceeded = bool(self.more_claims) + return self + class ReviewFull(ReviewEnvelope): """``GET /reviews/{review_id}``: the envelope plus every claim.""" @@ -1564,11 +2118,13 @@ class ReviewIssues(ReviewEnvelope): __all__ = [ + "NO_CHECKABLE_CLAIM", "AskHistory", "AskMessage", "AskReply", "AssessClaim", "AssessResponse", + "AssessStatus", "Assessment", "Audit", "BatchAccepted", @@ -1584,6 +2140,7 @@ class ReviewIssues(ReviewEnvelope): "Escalation", "EscalationPolicy", "ExtractStatus", + "ExtractedClaim", "ExtractedClaims", "ExtractedEntity", "FailureBlock", diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index 89eb1d9..c88741b 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -35,7 +35,15 @@ from typing import Any from .errors import LenzWebhookSignatureError -from .models import Citecheck, ReviewFull +from .models import ( + CandidateClaim, + Citecheck, + FailureBlock, + ReviewFull, + _new_code, + _old_code, + _with_completion_times, +) SIGNATURE_HEADER = "X-Lenz-Signature" SIGNATURE_PREFIX = "sha256=" @@ -87,11 +95,24 @@ def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool: # ── Typed events ───────────────────────────────────────────────────────── +# +# Two payload shapes reach the same events. The original one is flat for +# ``verification.*`` (``result``, ``needs_input``, ``error`` at the top) and +# nested for ``review.*`` / ``citecheck.*``. The newer one is one envelope for +# every event: ``event``, ``event_id``, the work's id, ``status``, and the body +# polling returns under the work's kind (``verification``, ``review``, +# ``citecheck``). Every attribute below is filled from either shape, and the +# original attributes keep their original meaning. @dataclass class WebhookEvent: - """Base class. Use ``isinstance`` to discriminate the union.""" + """Base class. Use ``isinstance`` to discriminate the union. + + ``event_id`` is the same on every delivery attempt of one event, while + ``attempt`` counts up: deduplicate on it. ``""`` when a payload does not + carry it (the original ``verification.*`` and ``certificate.*`` shape). + """ event: str task_id: str @@ -101,11 +122,18 @@ class WebhookEvent: batch_id: str | None = None status: str = "" raw: dict[str, Any] = field(default_factory=dict, repr=False) + # Keyword-only, so positional construction of every event keeps its order. + event_id: str = field(default="", kw_only=True) @dataclass class VerificationCompleted(WebhookEvent): - """``event=verification.completed`` — the pipeline produced a verdict.""" + """``event=verification.completed`` — the pipeline produced a verdict. + + ``result`` is the verification as a dict, the same body + ``verifications.get`` returns, as sent; from the newer payload shape, + which sends ``completed_at``, the deprecated ``modified_at`` is added. + """ result: dict[str, Any] = field(default_factory=dict) @@ -114,15 +142,22 @@ class VerificationCompleted(WebhookEvent): class VerificationFailed(WebhookEvent): """``event=verification.failed`` — the pipeline terminated without a verdict. - ``failure_class`` is WHY (closed set — ``upstream_unavailable`` | - ``insufficient_evidence`` | ``invalid_input`` | ``cancelled`` | - ``internal``); ``retryable`` is the derived signal (true iff - ``upstream_unavailable``). Both default when an older server omits them. + ``failure`` says why: ``failure.code`` is the cause (an open set), + ``failure.detail`` one sentence on what happened, ``failure.failure_class`` + the closed set (``upstream_unavailable`` | ``insufficient_evidence`` | + ``invalid_input`` | ``cancelled`` | ``internal``) and ``failure.retryable`` + the derived signal (true iff ``upstream_unavailable``). + + The original attributes keep their meaning: ``error`` is the cause code + (``not_a_claim`` where ``failure.code`` reads ``no_checkable_claim``), + and ``failure_class`` / ``retryable`` repeat the block's. They default when + an older server omits them. """ error: str = "" failure_class: str = "" retryable: bool | None = None + failure: FailureBlock | None = None @dataclass @@ -133,13 +168,18 @@ class VerificationNeedsInput(WebhookEvent): pipeline run produces a ``verification.completed`` (or another ``needs_input``) event. - ``hint`` is one sentence on what was unclear and how to resolve it - (``needs_input["hint"]`` on the wire); ``""`` when an older server omits - it. + ``reason`` is why (``multi_claim``), ``claims`` the options to pick from + (each with ``claim`` and ``domain``) and ``hint`` one sentence on what was + unclear and how to resolve it (``""`` when an older server omits it). + ``needs_input`` is the original dict, ``{"reason", "claims", "hint"}``, + whose options carry the claim as ``text``: as sent, or built from the + newer payload shape. """ needs_input: dict[str, Any] = field(default_factory=dict) hint: str = "" + reason: str = "" + claims: list[CandidateClaim] = field(default_factory=list) @dataclass @@ -171,8 +211,9 @@ class ReviewEvent(WebhookEvent): ``review.issues``. Deduplicate on ``event_id``: it is the same on every delivery attempt of - one event, while ``attempt`` counts up. ``task_id`` identifies the - delivery and cannot be polled on ``/verify/status``. The deep checks a + one event, while ``attempt`` counts up. ``task_id`` (deprecated) + identifies the delivery and cannot be polled on ``/verify/status``; a + payload that does not carry it reads ``event_id`` there. The deep checks a review runs send no ``verification.*`` events of their own. """ @@ -189,8 +230,9 @@ class CitecheckEvent(WebhookEvent): ``citecheck`` is the final check (the same body ``get_citecheck`` returns), or ``None`` if it could not be parsed (``raw["citecheck"]`` still has it). Deduplicate on ``event_id``: it is the same on every - delivery attempt of one event. ``task_id`` identifies the delivery and - cannot be polled on ``/verify/status``. + delivery attempt of one event. ``task_id`` (deprecated) identifies the + delivery and cannot be polled on ``/verify/status``; a payload that does + not carry it reads ``event_id`` there. """ event_id: str = "" @@ -198,120 +240,127 @@ class CitecheckEvent(WebhookEvent): citecheck: Citecheck | None = None +def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _opt_id(*values: Any) -> str | None: + for value in values: + if value: + return str(value) + return None + + +def _legacy_needs_input(payload: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]: + """The original ``needs_input`` dict: as sent, or built from the newer + envelope's polled body (each option carrying the claim as ``text``).""" + if isinstance(payload.get("needs_input"), dict): + return payload["needs_input"] # type: ignore[no-any-return] + claims = [] + for option in body.get("claims") or []: + if isinstance(option, dict): + claims.append({"text": option.get("claim", option.get("text")), "domain": option.get("domain", "")}) + out: dict[str, Any] = {"reason": body.get("reason", "")} + out["claims"] = claims + out["hint"] = body.get("hint", "") + return out + + def _build_event(payload: dict[str, Any]) -> WebhookEvent: """Discriminate on ``event`` and return the right typed dataclass.""" event = str(payload.get("event") or "") - task_id = str(payload.get("task_id") or "") + # The newer envelope nests the polled body under the work's kind. + body = _dict(payload.get("verification")) + result = _dict(payload.get("result")) or _dict(body.get("result")) + event_id = str(payload.get("event_id") or "") + task_id = str(payload.get("task_id") or body.get("task_id") or "") try: attempt = int(payload.get("attempt") or 1) except (TypeError, ValueError): attempt = 1 delivered_at = str(payload.get("delivered_at") or "") - verification_id = str(payload["verification_id"]) if payload.get("verification_id") else None - batch_id = str(payload["batch_id"]) if payload.get("batch_id") else None - status = str(payload.get("status") or "") + verification_id = _opt_id(payload.get("verification_id"), result.get("verification_id")) + batch_id = _opt_id(payload.get("batch_id"), body.get("batch_id")) + status = str(payload.get("status") or body.get("status") or "") + common: dict[str, Any] = { + "event": event, + "task_id": task_id, + "attempt": attempt, + "delivered_at": delivered_at, + "verification_id": verification_id, + "batch_id": batch_id, + "status": status, + "raw": payload, + "event_id": event_id, + } if event == "verification.completed": - return VerificationCompleted( - event=event, - task_id=task_id, - attempt=attempt, - delivered_at=delivered_at, - verification_id=verification_id, - batch_id=batch_id, - status=status, - raw=payload, - result=payload.get("result") or {}, - ) + if result and "modified_at" not in result: + # The newer shape: add the original ``modified_at`` (computed), + # leaving an original-shape result exactly as sent. + result = _with_completion_times(result) + return VerificationCompleted(**common, result=result) if event == "verification.failed": + block = _dict(body.get("failure")) or _dict(payload.get("failure")) + failure = FailureBlock.model_validate(block) if block else None + legacy_error = payload.get("error") + error = legacy_error if isinstance(legacy_error, str) and legacy_error else "" + if not error and failure is not None: + error = str(_old_code(failure.code, "not_a_claim") or "") + failure_class = payload.get("failure_class") or (failure.failure_class if failure else "") or "" + retryable = payload.get("retryable") + if not isinstance(retryable, bool): + retryable = failure.retryable if failure is not None else None + if failure is None and (error or failure_class): + # The original shape: the block from the flat fields. + failure = FailureBlock.model_validate( + {"code": _new_code(error), "failure_class": failure_class, "retryable": retryable} + ) return VerificationFailed( - event=event, - task_id=task_id, - attempt=attempt, - delivered_at=delivered_at, - verification_id=verification_id, - batch_id=batch_id, - status=status, - raw=payload, - error=str(payload.get("error") or ""), - failure_class=str(payload.get("failure_class") or ""), - retryable=payload.get("retryable") if isinstance(payload.get("retryable"), bool) else None, + **common, + error=error, + failure_class=str(failure_class), + retryable=retryable if isinstance(retryable, bool) else None, + failure=failure, ) if event == "verification.needs_input": - needs_input = payload.get("needs_input") or {} + needs_input = _legacy_needs_input(payload, body) return VerificationNeedsInput( - event=event, - task_id=task_id, - attempt=attempt, - delivered_at=delivered_at, - verification_id=verification_id, - batch_id=batch_id, - status=status, - raw=payload, + **common, needs_input=needs_input, - hint=str(needs_input.get("hint") or "") if isinstance(needs_input, dict) else "", + hint=str(needs_input.get("hint") or ""), + reason=str(needs_input.get("reason") or ""), + claims=[CandidateClaim.model_validate(c) for c in needs_input.get("claims") or [] if isinstance(c, dict)], ) if event == "certificate.timestamped": - return CertificateTimestamped( - event=event, - task_id=task_id, - attempt=attempt, - delivered_at=delivered_at, - verification_id=verification_id, - batch_id=batch_id, - status=status, - raw=payload, - coverage=payload.get("coverage") or {}, - ) + coverage = _dict(payload.get("coverage")) or _dict(body.get("coverage")) or _dict(result.get("coverage")) + return CertificateTimestamped(**common, coverage=coverage) if event in ("review.completed", "review.failed"): - body = payload.get("review") + review_body = payload.get("review") try: - review = ReviewFull.model_validate(body) if isinstance(body, dict) else None + review = ReviewFull.model_validate(review_body) if isinstance(review_body, dict) else None except ValueError: review = None + common["task_id"] = task_id or event_id return ReviewEvent( - event=event, - task_id=task_id, - attempt=attempt, - delivered_at=delivered_at, - verification_id=verification_id, - batch_id=batch_id, - status=status, - raw=payload, - event_id=str(payload.get("event_id") or ""), - review_id=str(payload.get("review_id") or ""), + **common, + review_id=str(payload.get("review_id") or _dict(review_body).get("review_id") or ""), review=review, ) if event in ("citecheck.completed", "citecheck.failed"): - body = payload.get("citecheck") + check_body = payload.get("citecheck") try: - check = Citecheck.model_validate(body) if isinstance(body, dict) else None + check = Citecheck.model_validate(check_body) if isinstance(check_body, dict) else None except ValueError: check = None + common["task_id"] = task_id or event_id return CitecheckEvent( - event=event, - task_id=task_id, - attempt=attempt, - delivered_at=delivered_at, - verification_id=verification_id, - batch_id=batch_id, - status=status, - raw=payload, - event_id=str(payload.get("event_id") or ""), - citecheck_id=str(payload.get("citecheck_id") or ""), + **common, + citecheck_id=str(payload.get("citecheck_id") or _dict(check_body).get("citecheck_id") or ""), citecheck=check, ) # Unknown event type — return generic. Future-compatible: ignore events # you do not handle rather than failing the delivery. - return WebhookEvent( - event=event, - task_id=task_id, - attempt=attempt, - delivered_at=delivered_at, - verification_id=verification_id, - batch_id=batch_id, - status=status, - raw=payload, - ) + return WebhookEvent(**common) def parse_webhook(body: bytes | str | dict[str, Any]) -> WebhookEvent: diff --git a/tests/fixtures/parity/canonical/account__library_empty.json b/tests/fixtures/parity/canonical/account__library_empty.json new file mode 100644 index 0000000..b419316 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__library_empty.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 0, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/account__library_item_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/account__library_item_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..1045857 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__library_item_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "A library claim.", + "domain": "", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "completed_at": "2026-10-02T00:03:00.123456+00:00", + "language": "en" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/account__library_item_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/account__library_item_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..d4d580f --- /dev/null +++ b/tests/fixtures/parity/canonical/account__library_item_modified_at_same_day_hours_apart.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "A library claim.", + "domain": "", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "completed_at": "2026-10-01T20:00:00.123456+00:00", + "language": "en" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/account__library_items.json b/tests/fixtures/parity/canonical/account__library_items.json new file mode 100644 index 0000000..e810be9 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__library_items.json @@ -0,0 +1,60 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "Library claim 0.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", + "language": "en" + }, + { + "verification_id": "7c83247c", + "claim": "Library claim 1.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:28:00.123456+00:00", + "completed_at": "2026-10-01T09:28:00.123456+00:00", + "language": "en" + }, + { + "verification_id": "b79019e4", + "claim": "Library claim 2.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:42:00.123456+00:00", + "completed_at": "2026-10-01T09:42:00.123456+00:00", + "language": "en" + } + ], + "total": 3, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/account__library_no_match.json b/tests/fixtures/parity/canonical/account__library_no_match.json new file mode 100644 index 0000000..b419316 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__library_no_match.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 0, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/account__library_page_past_end.json b/tests/fixtures/parity/canonical/account__library_page_past_end.json new file mode 100644 index 0000000..86d64c7 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__library_page_past_end.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 1, + "page": 2, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/account__library_paginated_second_page.json b/tests/fixtures/parity/canonical/account__library_paginated_second_page.json new file mode 100644 index 0000000..ae61ec7 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__library_paginated_second_page.json @@ -0,0 +1,45 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "Paged claim 020.", + "domain": "", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", + "language": "en" + }, + { + "verification_id": "7c83247c", + "claim": "Paged claim 021.", + "domain": "", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:28:00.123456+00:00", + "completed_at": "2026-10-01T09:28:00.123456+00:00", + "language": "en" + } + ], + "total": 22, + "page": 2, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_extra_credits.json b/tests/fixtures/parity/canonical/account__me_usage_extra_credits.json new file mode 100644 index 0000000..14ab818 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_extra_credits.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "credits": { + "total": 350, + "used": 0, + "remaining": 350, + "extra": 250, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_extra_only.json b/tests/fixtures/parity/canonical/account__me_usage_extra_only.json new file mode 100644 index 0000000..3b3e240 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_extra_only.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "credits": { + "total": 137, + "used": 100, + "remaining": 37, + "extra": 37, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_extract_calls.json b/tests/fixtures/parity/canonical/account__me_usage_extract_calls.json new file mode 100644 index 0000000..54303ab --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_extract_calls.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "credits": { + "total": 100, + "used": 0, + "remaining": 100, + "extra": 0, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 7, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_free.json b/tests/fixtures/parity/canonical/account__me_usage_free.json new file mode 100644 index 0000000..e9b9231 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_free.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "credits": { + "total": 100, + "used": 0, + "remaining": 100, + "extra": 0, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_free_partly_spent.json b/tests/fixtures/parity/canonical/account__me_usage_free_partly_spent.json new file mode 100644 index 0000000..d08a9ac --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_free_partly_spent.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "credits": { + "total": 100, + "used": 17, + "remaining": 83, + "extra": 0, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_plus.json b/tests/fixtures/parity/canonical/account__me_usage_plus.json new file mode 100644 index 0000000..4a255a9 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_plus.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "plus", + "plan_label": "Plus", + "credits": { + "total": 500, + "used": 0, + "remaining": 500, + "extra": 0, + "resets_at": "2026-10-01T09:07:00.123456+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_pro.json b/tests/fixtures/parity/canonical/account__me_usage_pro.json new file mode 100644 index 0000000..943f796 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_pro.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "pro", + "plan_label": "Pro", + "credits": { + "total": 5000, + "used": 0, + "remaining": 5000, + "extra": 0, + "resets_at": "2026-10-01T09:07:00.123456+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_pro_extra.json b/tests/fixtures/parity/canonical/account__me_usage_pro_extra.json new file mode 100644 index 0000000..8f8d482 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_pro_extra.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "pro", + "plan_label": "Pro", + "credits": { + "total": 6000, + "used": 0, + "remaining": 6000, + "extra": 1000, + "resets_at": "2026-10-01T09:07:00.123456+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_stale_quota_row.json b/tests/fixtures/parity/canonical/account__me_usage_stale_quota_row.json new file mode 100644 index 0000000..e9b9231 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_stale_quota_row.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "credits": { + "total": 100, + "used": 0, + "remaining": 100, + "extra": 0, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/canonical/assess__401_no_key.json b/tests/fixtures/parity/canonical/assess__401_no_key.json new file mode 100644 index 0000000..1c3df51 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__401_no_key.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + } +} diff --git a/tests/fixtures/parity/canonical/assess__402_no_credits.json b/tests/fixtures/parity/canonical/assess__402_no_credits.json new file mode 100644 index 0000000..5e931cc --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__402_no_credits.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining /assess units.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/canonical/assess__422_blank_item.json b/tests/fixtures/parity/canonical/assess__422_blank_item.json new file mode 100644 index 0000000..db514d2 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__422_blank_item.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims[1] is blank.", + "code": "blank_input", + "errors": [ + { + "loc": [ + "body", + "claims", + 1 + ], + "msg": "claims[1] is blank.", + "type": "blank_input" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/assess__422_blank_text.json b/tests/fixtures/parity/canonical/assess__422_blank_text.json new file mode 100644 index 0000000..a5a1062 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__422_blank_text.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claim is required.", + "code": "blank_input", + "errors": [ + { + "loc": [ + "body", + "claim" + ], + "msg": "claim is required.", + "type": "blank_input" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/assess__422_input_conflict.json b/tests/fixtures/parity/canonical/assess__422_input_conflict.json new file mode 100644 index 0000000..a46942a --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__422_input_conflict.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Send either `claim` (one text) or `claims` (a list of claims), not both.", + "code": "input_conflict", + "errors": [ + { + "loc": [ + "body", + "claims" + ], + "msg": "Send either `claim` (one text) or `claims` (a list of claims), not both.", + "type": "input_conflict" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/assess__422_item_too_long.json b/tests/fixtures/parity/canonical/assess__422_item_too_long.json new file mode 100644 index 0000000..137d44e --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__422_item_too_long.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.", + "code": "item_too_long", + "errors": [ + { + "loc": [ + "body", + "claims", + 1 + ], + "msg": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.", + "type": "item_too_long" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/assess__422_no_input_field.json b/tests/fixtures/parity/canonical/assess__422_no_input_field.json new file mode 100644 index 0000000..9136471 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__422_no_input_field.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claim: Field required", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/assess__422_too_many_items.json b/tests/fixtures/parity/canonical/assess__422_too_many_items.json new file mode 100644 index 0000000..3468b9e --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__422_too_many_items.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "At most 20 claims per call; got 21. Split the list.", + "code": "too_many_items", + "errors": [ + { + "loc": [ + "body", + "claims" + ], + "msg": "At most 20 claims per call; got 21. Split the list.", + "type": "too_many_items" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/assess__422_wrong_type_claim.json b/tests/fixtures/parity/canonical/assess__422_wrong_type_claim.json new file mode 100644 index 0000000..acbd190 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__422_wrong_type_claim.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claim: Input should be a valid string", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "claim" + ], + "msg": "Input should be a valid string", + "type": "string_type" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/assess__422_wrong_type_claims.json b/tests/fixtures/parity/canonical/assess__422_wrong_type_claims.json new file mode 100644 index 0000000..2b44c06 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__422_wrong_type_claims.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims: Input should be a valid list", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "claims" + ], + "msg": "Input should be a valid list", + "type": "list_type" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/assess__502_framing_failed.json b/tests/fixtures/parity/canonical/assess__502_framing_failed.json new file mode 100644 index 0000000..e671a1e --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__502_framing_failed.json @@ -0,0 +1,12 @@ +{ + "status": 502, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Framing failed", + "code": "framing_failed" + } +} diff --git a/tests/fixtures/parity/canonical/assess__503_capacity.json b/tests/fixtures/parity/canonical/assess__503_capacity.json new file mode 100644 index 0000000..343e0e1 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__503_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "100", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 100, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/assess__503_list_all_upstream_unavailable.json b/tests/fixtures/parity/canonical/assess__503_list_all_upstream_unavailable.json new file mode 100644 index 0000000..c19b99f --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__503_list_all_upstream_unavailable.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Our model providers are temporarily unavailable. Please retry shortly. Nothing was charged.", + "code": "upstream_unavailable", + "retry_after": 90, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/assess__503_upstream_unavailable.json b/tests/fixtures/parity/canonical/assess__503_upstream_unavailable.json new file mode 100644 index 0000000..3315806 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__503_upstream_unavailable.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Our model providers are temporarily unavailable. Please retry shortly.", + "code": "upstream_unavailable", + "retry_after": 90, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/assess__deep_tier_hit.json b/tests/fixtures/parity/canonical/assess__deep_tier_hit.json new file mode 100644 index 0000000..0a05b10 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__deep_tier_hit.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "Water boils at 100C.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": "https://lenz.io/api/v1/verifications/f3436f50", + "rationale": "The claim is verified.", + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__idempotency_body_mismatch.json b/tests/fixtures/parity/canonical/assess__idempotency_body_mismatch.json new file mode 100644 index 0000000..e20089e --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__idempotency_body_mismatch.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + } +} diff --git a/tests/fixtures/parity/canonical/assess__idempotency_first.json b/tests/fixtures/parity/canonical/assess__idempotency_first.json new file mode 100644 index 0000000..8c05be0 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__idempotency_first.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "The Earth orbits the Sun.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__idempotency_replay.json b/tests/fixtures/parity/canonical/assess__idempotency_replay.json new file mode 100644 index 0000000..8c05be0 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__idempotency_replay.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "The Earth orbits the Sun.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__list_all_error_rows.json b/tests/fixtures/parity/canonical/assess__list_all_error_rows.json new file mode 100644 index 0000000..75ced45 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__list_all_error_rows.json @@ -0,0 +1,55 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "no_checkable_claim", + "claims": [ + { + "claim": "hi", + "language": "en", + "status": "failed", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + } + }, + { + "claim": "hello", + "language": "en", + "status": "failed", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + } + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__list_compound_item.json b/tests/fixtures/parity/canonical/assess__list_compound_item.json new file mode 100644 index 0000000..536868b --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__list_compound_item.json @@ -0,0 +1,44 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "Primary claim.", + "language": "en", + "status": "completed", + "verdict": "Mostly False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [ + "Second claim.", + "Third claim." + ], + "failure": null + }, + { + "claim": "A plain claim.", + "language": "en", + "status": "completed", + "verdict": "Mostly False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__list_mixed_rows.json b/tests/fixtures/parity/canonical/assess__list_mixed_rows.json new file mode 100644 index 0000000..5cec126 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__list_mixed_rows.json @@ -0,0 +1,88 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "Water boils at 100 C at sea level.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "hello there", + "language": "en", + "status": "failed", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + } + }, + { + "claim": "vendor is down", + "language": "en", + "status": "failed", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + { + "claim": "cannot frame", + "language": "en", + "status": "failed", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": { + "code": "framing_failed", + "detail": "The input could not be read as a claim.", + "hint": "This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; nothing was charged.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + } + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__memo_hit.json b/tests/fixtures/parity/canonical/assess__memo_hit.json new file mode 100644 index 0000000..873c763 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__memo_hit.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "The Danube flows through Vienna.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": "The river runs through the city.", + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__replay_later_200.json b/tests/fixtures/parity/canonical/assess__replay_later_200.json new file mode 100644 index 0000000..bac5c15 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__replay_later_200.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "The Earth orbits the Sun.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__single_no_claim.json b/tests/fixtures/parity/canonical/assess__single_no_claim.json new file mode 100644 index 0000000..3a193c0 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__single_no_claim.json @@ -0,0 +1,21 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "no_checkable_claim", + "claims": [], + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__single_one_claim.json b/tests/fixtures/parity/canonical/assess__single_one_claim.json new file mode 100644 index 0000000..d27ac90 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__single_one_claim.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "The registry reported 4,200 filings in 2024.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": "The registry lists 4,200 filings for 2024 and has not revised the figure.", + "dissent": "The registry figure is provisional, so the 2024 total cannot yet be confirmed.", + "suggested_rewrite": null, + "more_claims": [], + "failure": null + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__single_text_over_wave_more_claims.json b/tests/fixtures/parity/canonical/assess__single_text_over_wave_more_claims.json new file mode 100644 index 0000000..1d4fc2e --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__single_text_over_wave_more_claims.json @@ -0,0 +1,278 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "Claim number 0 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 1 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 2 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 3 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 4 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 5 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 6 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 7 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 8 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 9 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 10 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 11 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 12 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 13 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 14 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 15 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 16 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 17 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 18 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "Claim number 19 is documented.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + } + ], + "failure": null, + "more_claims": [ + "Claim number 20 is documented.", + "Claim number 21 is documented." + ] + } +} diff --git a/tests/fixtures/parity/canonical/assess__single_text_several_claims.json b/tests/fixtures/parity/canonical/assess__single_text_several_claims.json new file mode 100644 index 0000000..4313609 --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__single_text_several_claims.json @@ -0,0 +1,54 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "Water boils at 100 C at sea level.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "The Moon is made of cheese.", + "language": "en", + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + { + "claim": "The Pacific is the deepest ocean.", + "language": "en", + "status": "completed", + "verdict": "Mostly True", + "confidence": "medium", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/assess__suggest_rewrite.json b/tests/fixtures/parity/canonical/assess__suggest_rewrite.json new file mode 100644 index 0000000..d2aee3e --- /dev/null +++ b/tests/fixtures/parity/canonical/assess__suggest_rewrite.json @@ -0,0 +1,41 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ok", + "claims": [ + { + "claim": "Venus is the closest planet to the Sun.", + "language": "en", + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": "Mercury, not Venus, is the closest planet to the Sun.", + "dissent": null, + "suggested_rewrite": "Mercury is the closest planet to the Sun.", + "more_claims": [], + "failure": null + }, + { + "claim": "Mercury is the closest planet to the Sun.", + "language": "en", + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": "Mercury is the innermost planet.", + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + } + ], + "failure": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__402_no_credits.json b/tests/fixtures/parity/canonical/citecheck__402_no_credits.json new file mode 100644 index 0000000..dfb7db4 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__402_no_credits.json @@ -0,0 +1,18 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining credits for citation checks.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 100, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_empty_body.json b/tests/fixtures/parity/canonical/citecheck__422_empty_body.json new file mode 100644 index 0000000..82cdb99 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_empty_body.json @@ -0,0 +1,25 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload: Value error, send exactly one of text and pairs", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload" + ], + "msg": "Value error, send exactly one of text and pairs", + "type": "value_error", + "ctx": { + "error": "send exactly one of text and pairs" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_invalid_doi.json b/tests/fixtures/parity/canonical/citecheck__422_invalid_doi.json new file mode 100644 index 0000000..c3c1d4c --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_invalid_doi.json @@ -0,0 +1,25 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "pairs[0].doi: Send the DOI alone, like 10.1038/nature12373.", + "code": "invalid_doi", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0, + "doi" + ], + "msg": "Send the DOI alone, like 10.1038/nature12373.", + "type": "invalid_doi" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_max_citations_over_20.json b/tests/fixtures/parity/canonical/citecheck__422_max_citations_over_20.json new file mode 100644 index 0000000..610a9cd --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_max_citations_over_20.json @@ -0,0 +1,26 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "max_citations: Input should be less than or equal to 20", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "max_citations" + ], + "msg": "Input should be less than or equal to 20", + "type": "less_than_equal", + "ctx": { + "le": 20 + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_max_citations_with_pairs.json b/tests/fixtures/parity/canonical/citecheck__422_max_citations_with_pairs.json new file mode 100644 index 0000000..746f21f --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_max_citations_with_pairs.json @@ -0,0 +1,25 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload: Value error, max_citations goes with text: every pair is checked", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload" + ], + "msg": "Value error, max_citations goes with text: every pair is checked", + "type": "value_error", + "ctx": { + "error": "max_citations goes with text: every pair is checked" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_pair_bad_url_scheme.json b/tests/fixtures/parity/canonical/citecheck__422_pair_bad_url_scheme.json new file mode 100644 index 0000000..a791f53 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_pair_bad_url_scheme.json @@ -0,0 +1,28 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "pairs[0].url: Value error, url: an http or https link", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0, + "url" + ], + "msg": "Value error, url: an http or https link", + "type": "value_error", + "ctx": { + "error": "url: an http or https link" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_pair_no_source.json b/tests/fixtures/parity/canonical/citecheck__422_pair_no_source.json new file mode 100644 index 0000000..f7ac540 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_pair_no_source.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "pairs[0]: Value error, send exactly one of url and doi", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0 + ], + "msg": "Value error, send exactly one of url and doi", + "type": "value_error", + "ctx": { + "error": "send exactly one of url and doi" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_pair_url_and_doi.json b/tests/fixtures/parity/canonical/citecheck__422_pair_url_and_doi.json new file mode 100644 index 0000000..f7ac540 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_pair_url_and_doi.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "pairs[0]: Value error, send exactly one of url and doi", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0 + ], + "msg": "Value error, send exactly one of url and doi", + "type": "value_error", + "ctx": { + "error": "send exactly one of url and doi" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_pairs_empty.json b/tests/fixtures/parity/canonical/citecheck__422_pairs_empty.json new file mode 100644 index 0000000..9b2008e --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_pairs_empty.json @@ -0,0 +1,26 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "pairs: Value error, pairs: 1 to 20 items", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs" + ], + "msg": "Value error, pairs: 1 to 20 items", + "type": "value_error", + "ctx": { + "error": "pairs: 1 to 20 items" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_pairs_over_20.json b/tests/fixtures/parity/canonical/citecheck__422_pairs_over_20.json new file mode 100644 index 0000000..9b2008e --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_pairs_over_20.json @@ -0,0 +1,26 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "pairs: Value error, pairs: 1 to 20 items", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs" + ], + "msg": "Value error, pairs: 1 to 20 items", + "type": "value_error", + "ctx": { + "error": "pairs: 1 to 20 items" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_quote_not_in_statement.json b/tests/fixtures/parity/canonical/citecheck__422_quote_not_in_statement.json new file mode 100644 index 0000000..e4d2408 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_quote_not_in_statement.json @@ -0,0 +1,24 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "pairs[0]: Each quote must be words of its statement.", + "code": "quote_not_in_statement", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0 + ], + "msg": "Each quote must be words of its statement.", + "type": "quote_not_in_statement" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_text_and_pairs.json b/tests/fixtures/parity/canonical/citecheck__422_text_and_pairs.json new file mode 100644 index 0000000..82cdb99 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_text_and_pairs.json @@ -0,0 +1,25 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload: Value error, send exactly one of text and pairs", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload" + ], + "msg": "Value error, send exactly one of text and pairs", + "type": "value_error", + "ctx": { + "error": "send exactly one of text and pairs" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_unknown_field.json b/tests/fixtures/parity/canonical/citecheck__422_unknown_field.json new file mode 100644 index 0000000..0aed242 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_unknown_field.json @@ -0,0 +1,25 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "pairs[0].extra: Extra inputs are not permitted", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0, + "extra" + ], + "msg": "Extra inputs are not permitted", + "type": "extra_forbidden" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__422_url_input.json b/tests/fixtures/parity/canonical/citecheck__422_url_input.json new file mode 100644 index 0000000..816edbe --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_url_input.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Send the page's text, with its links, to check the sources it cites.", + "code": "url_input", + "errors": [ + { + "loc": [ + "body", + "text" + ], + "msg": "Send the page's text, with its links, to check the sources it cites.", + "type": "url_input" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__429_citecheck_in_flight.json b/tests/fixtures/parity/canonical/citecheck__429_citecheck_in_flight.json new file mode 100644 index 0000000..fa73820 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__429_citecheck_in_flight.json @@ -0,0 +1,14 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This account already has 3 citation checks running. Wait for one to finish.", + "code": "citecheck_in_flight", + "retry_after": 60 + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__503_capacity.json b/tests/fixtures/parity/canonical/citecheck__503_capacity.json new file mode 100644 index 0000000..bcff7db --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__503_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 90, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__503_citations_unavailable.json b/tests/fixtures/parity/canonical/citecheck__503_citations_unavailable.json new file mode 100644 index 0000000..cdeb748 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__503_citations_unavailable.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "300", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Citation checking is switched off right now. Retry later; nothing was charged.", + "code": "citations_unavailable", + "retry_after": 300, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_404_not_found.json b/tests/fixtures/parity/canonical/citecheck__get_404_not_found.json new file mode 100644 index 0000000..e0adfb3 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_404_not_found.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Citation check not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_404_review_id.json b/tests/fixtures/parity/canonical/citecheck__get_404_review_id.json new file mode 100644 index 0000000..e0adfb3 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_404_review_id.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Citation check not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_410_purged.json b/tests/fixtures/parity/canonical/citecheck__get_410_purged.json new file mode 100644 index 0000000..9fff981 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_410_purged.json @@ -0,0 +1,13 @@ +{ + "status": 410, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This check is no longer available: its account removes content after a set period.", + "code": "purged", + "purged_at": "2026-10-01T09:07:00.123456Z" + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_checking.json b/tests/fixtures/parity/canonical/citecheck__get_checking.json new file mode 100644 index 0000000..9ca9e92 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_checking.json @@ -0,0 +1,103 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "checking", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "poll_after_seconds": 10, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_clean.json b/tests/fixtures/parity/canonical/citecheck__get_completed_clean.json new file mode 100644 index 0000000..11e7ed2 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_clean.json @@ -0,0 +1,111 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_doi_pair.json b/tests/fixtures/parity/canonical/citecheck__get_completed_doi_pair.json new file mode 100644 index 0000000..49e62df --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_doi_pair.json @@ -0,0 +1,76 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 1 + }, + "summary": { + "citations_found": 1, + "citations_selected": 1, + "citation_limit": 1, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 1, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 1 + }, + "citations": [ + { + "index": 0, + "reference": "10.1038/nature12373", + "cited_url": "https://doi.org/10.1038/nature12373", + "doi": "10.1038/nature12373", + "statement": "Diamond sensors measure heat in cells.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at None", + "page_published_date": null, + "page_language": "en", + "source_url": null, + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_incomplete.json b/tests/fixtures/parity/canonical/citecheck__get_completed_incomplete.json new file mode 100644 index 0000000..a50708c --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_incomplete.json @@ -0,0 +1,129 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 1, + "unchecked": 0, + "failed": 1 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 1 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "The check failed on our side. Try again later.", + "failure": { + "code": "internal", + "detail": "The check failed on our side.", + "hint": "The check failed on our side. Try again later.", + "failure_class": "internal", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#internal" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "code": "internal", + "detail": "The check failed on our side.", + "hint": "The check failed on our side. Try again later.", + "failure_class": "internal", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#internal" + } + } + ], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_issues_found.json b/tests/fixtures/parity/canonical/citecheck__get_completed_issues_found.json new file mode 100644 index 0000000..1555b9a --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_issues_found.json @@ -0,0 +1,141 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 20 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 1 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "contradicted", + "source": "support", + "is_issue": true + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "contradicted", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "finding": "contradicted", + "source": "support", + "snippet": "The source says so.", + "rationale": "The page states it.", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "failure": null + } + ], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_partly_supported.json b/tests/fixtures/parity/canonical/citecheck__get_completed_partly_supported.json new file mode 100644 index 0000000..f1d95be --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_partly_supported.json @@ -0,0 +1,111 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "partly_supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "partly_supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_unchecked.json b/tests/fixtures/parity/canonical/citecheck__get_completed_unchecked.json new file mode 100644 index 0000000..a37c4cb --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_unchecked.json @@ -0,0 +1,111 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 2, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "unchecked", + "source": null, + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "unchecked", + "snippet": null, + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": "no_text", + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": { + "finding": "unchecked", + "source": null, + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "unchecked", + "snippet": null, + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": "no_text", + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_failed_no_citations.json b/tests/fixtures/parity/canonical/citecheck__get_failed_no_citations.json new file mode 100644 index 0000000..25625db --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_failed_no_citations.json @@ -0,0 +1,43 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 20 + }, + "summary": { + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [], + "citation_issues": [], + "citation_failures": [], + "failure": { + "code": "no_citations", + "detail": "The text holds no citation to check.", + "hint": "The text has no citation to check: no link, DOI or numbered reference with one.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_failed_upstream_unavailable.json b/tests/fixtures/parity/canonical/citecheck__get_failed_upstream_unavailable.json new file mode 100644 index 0000000..a323d29 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_failed_upstream_unavailable.json @@ -0,0 +1,153 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 20 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 2 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": null, + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "Lenz could not reach this source just now. Try again later.", + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "Lenz could not reach this source just now. Try again later.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": null, + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "Lenz could not reach this source just now. Try again later.", + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "Lenz could not reach this source just now. Try again later.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "Lenz could not reach this source just now. Try again later.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + { + "citation_index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "Lenz could not reach this source just now. Try again later.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } + ], + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": null, + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_queued.json b/tests/fixtures/parity/canonical/citecheck__get_queued.json new file mode 100644 index 0000000..df7dfee --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_queued.json @@ -0,0 +1,72 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "queued", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "poll_after_seconds": 10, + "policy": { + "max_citations": 1 + }, + "summary": { + "citations_found": 1, + "citations_selected": 1, + "citation_limit": 1, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_text_limit_exactly_at_limit.json b/tests/fixtures/parity/canonical/citecheck__get_text_limit_exactly_at_limit.json new file mode 100644 index 0000000..600d1b9 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_text_limit_exactly_at_limit.json @@ -0,0 +1,119 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__get_text_limit_reached.json b/tests/fixtures/parity/canonical/citecheck__get_text_limit_reached.json new file mode 100644 index 0000000..a4dbb6b --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_text_limit_reached.json @@ -0,0 +1,93 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 1 + }, + "summary": { + "citations_found": 2, + "citations_selected": 1, + "citation_limit": 1, + "citation_limit_exceeded": true, + "citation_checks": { + "checked": 1, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 1 + }, + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [ + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "sentence": "The agency said so in a statement.", + "position": { + "start": 111, + "end": 178, + "text": null + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__idempotency_body_mismatch_422.json b/tests/fixtures/parity/canonical/citecheck__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..e20089e --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__idempotency_body_mismatch_422.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__idempotency_conflict_409.json b/tests/fixtures/parity/canonical/citecheck__idempotency_conflict_409.json new file mode 100644 index 0000000..3978a83 --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__idempotency_conflict_409.json @@ -0,0 +1,13 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A check with this Idempotency-Key is still being created. Retry shortly.", + "code": "idempotency_conflict", + "citecheck_id": null + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__idempotent_replay_202.json b/tests/fixtures/parity/canonical/citecheck__idempotent_replay_202.json new file mode 100644 index 0000000..864551e --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__idempotent_replay_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/citechecks/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__receipt_202.json b/tests/fixtures/parity/canonical/citecheck__receipt_202.json new file mode 100644 index 0000000..864551e --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__receipt_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/citechecks/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/canonical/citecheck__receipt_202_empty_webhook_url.json new file mode 100644 index 0000000..864551e --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__receipt_202_empty_webhook_url.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/citechecks/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/errors__ask_bad_language.json b/tests/fixtures/parity/canonical/errors__ask_bad_language.json new file mode 100644 index 0000000..e543e6d --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__ask_bad_language.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "code": "unsupported_language", + "errors": [ + { + "loc": [ + "body", + "language" + ], + "msg": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "type": "unsupported_language" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/errors__ask_empty_message.json b/tests/fixtures/parity/canonical/errors__ask_empty_message.json new file mode 100644 index 0000000..86f7435 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__ask_empty_message.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Message cannot be empty.", + "code": "blank_input", + "errors": [ + { + "loc": [ + "body", + "message" + ], + "msg": "Message cannot be empty.", + "type": "blank_input" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/errors__ask_failed.json b/tests/fixtures/parity/canonical/errors__ask_failed.json new file mode 100644 index 0000000..df45b1c --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__ask_failed.json @@ -0,0 +1,12 @@ +{ + "status": 502, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The chat model could not answer this question.", + "code": "ask_failed" + } +} diff --git a/tests/fixtures/parity/canonical/errors__ask_idempotency_key_reused.json b/tests/fixtures/parity/canonical/errors__ask_idempotency_key_reused.json new file mode 100644 index 0000000..e20089e --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__ask_idempotency_key_reused.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + } +} diff --git a/tests/fixtures/parity/canonical/errors__ask_message_too_long.json b/tests/fixtures/parity/canonical/errors__ask_message_too_long.json new file mode 100644 index 0000000..80b5bab --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__ask_message_too_long.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Message too long (max 500 characters).", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "message" + ], + "msg": "Message too long (max 500 characters).", + "type": "validation_error" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/errors__ask_not_completed.json b/tests/fixtures/parity/canonical/errors__ask_not_completed.json new file mode 100644 index 0000000..9800c71 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__ask_not_completed.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Ask is only available for completed verifications.", + "code": "verification_not_ready" + } +} diff --git a/tests/fixtures/parity/canonical/errors__ask_reset_not_found.json b/tests/fixtures/parity/canonical/errors__ask_reset_not_found.json new file mode 100644 index 0000000..3eec577 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__ask_reset_not_found.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Verification not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/errors__ask_send_not_found.json b/tests/fixtures/parity/canonical/errors__ask_send_not_found.json new file mode 100644 index 0000000..3eec577 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__ask_send_not_found.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Verification not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_bad_key.json b/tests/fixtures/parity/canonical/errors__auth_bad_key.json new file mode 100644 index 0000000..1c3df51 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_bad_key.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_basic_scheme.json b/tests/fixtures/parity/canonical/errors__auth_basic_scheme.json new file mode 100644 index 0000000..1c3df51 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_basic_scheme.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_insufficient_scope.json b/tests/fixtures/parity/canonical/errors__auth_insufficient_scope.json new file mode 100644 index 0000000..15b8696 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_insufficient_scope.json @@ -0,0 +1,14 @@ +{ + "status": 403, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"verify\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "verify" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_missing_bearer.json b/tests/fixtures/parity/canonical/errors__auth_missing_bearer.json new file mode 100644 index 0000000..1c3df51 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_missing_bearer.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_missing_bearer_post.json b/tests/fixtures/parity/canonical/errors__auth_missing_bearer_post.json new file mode 100644 index 0000000..1c3df51 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_missing_bearer_post.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_history.json b/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_history.json new file mode 100644 index 0000000..3445e87 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_history.json @@ -0,0 +1,12 @@ +{ + "status": 403, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This report is private.", + "code": "private_claim" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_send.json b/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_send.json new file mode 100644 index 0000000..3445e87 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_send.json @@ -0,0 +1,12 @@ +{ + "status": 403, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This report is private.", + "code": "private_claim" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_unrecognised_prefix.json b/tests/fixtures/parity/canonical/errors__auth_unrecognised_prefix.json new file mode 100644 index 0000000..1c3df51 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_unrecognised_prefix.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + } +} diff --git a/tests/fixtures/parity/canonical/errors__http_error_library_bad_curated.json b/tests/fixtures/parity/canonical/errors__http_error_library_bad_curated.json new file mode 100644 index 0000000..76cbe7a --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__http_error_library_bad_curated.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Invalid curated list(s). Allowed: trivia", + "code": "validation_error", + "errors": [ + { + "loc": [ + "query", + "curated" + ], + "msg": "Invalid curated list(s). Allowed: trivia", + "type": "validation_error" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/errors__http_error_library_bad_verdict.json b/tests/fixtures/parity/canonical/errors__http_error_library_bad_verdict.json new file mode 100644 index 0000000..09dbe2e --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__http_error_library_bad_verdict.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True", + "code": "validation_error", + "errors": [ + { + "loc": [ + "query", + "verdict" + ], + "msg": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True", + "type": "validation_error" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/errors__not_found_ask.json b/tests/fixtures/parity/canonical/errors__not_found_ask.json new file mode 100644 index 0000000..3eec577 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__not_found_ask.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Verification not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/errors__not_found_route.json b/tests/fixtures/parity/canonical/errors__not_found_route.json new file mode 100644 index 0000000..f1a8c33 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__not_found_route.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/errors__not_found_verification.json b/tests/fixtures/parity/canonical/errors__not_found_verification.json new file mode 100644 index 0000000..f1a8c33 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__not_found_verification.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/errors__not_found_verify_status.json b/tests/fixtures/parity/canonical/errors__not_found_verify_status.json new file mode 100644 index 0000000..ed0bc13 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__not_found_verify_status.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Task not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/errors__payment_required_ask.json b/tests/fixtures/parity/canonical/errors__payment_required_ask.json new file mode 100644 index 0000000..e4bc4cf --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__payment_required_ask.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining ask credits.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/canonical/errors__payment_required_assess.json b/tests/fixtures/parity/canonical/errors__payment_required_assess.json new file mode 100644 index 0000000..5e931cc --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__payment_required_assess.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining /assess units.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/canonical/errors__payment_required_verify.json b/tests/fixtures/parity/canonical/errors__payment_required_verify.json new file mode 100644 index 0000000..1f73a67 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__payment_required_verify.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining claim checks.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 10 + } +} diff --git a/tests/fixtures/parity/canonical/errors__payment_required_verify_batch_short.json b/tests/fixtures/parity/canonical/errors__payment_required_verify_batch_short.json new file mode 100644 index 0000000..e80d840 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__payment_required_verify_batch_short.json @@ -0,0 +1,20 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Insufficient credits for batch.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "requested": 5, + "remaining": 3, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 30, + "cost": 50 + } +} diff --git a/tests/fixtures/parity/canonical/errors__rate_limited_extract.json b/tests/fixtures/parity/canonical/errors__rate_limited_extract.json new file mode 100644 index 0000000..e45b708 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__rate_limited_extract.json @@ -0,0 +1,17 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "900", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "code": "extract_daily_limit", + "limit": 1000, + "retry_after": 900, + "docs_url": "https://lenz.io/docs/errors#rate-limits", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/canonical/errors__service_unavailable_ask.json b/tests/fixtures/parity/canonical/errors__service_unavailable_ask.json new file mode 100644 index 0000000..94a09cd --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__service_unavailable_ask.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The chat model is unavailable right now. Retry shortly.", + "code": "upstream_unavailable", + "retry_after": 90, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/errors__service_unavailable_capacity.json b/tests/fixtures/parity/canonical/errors__service_unavailable_capacity.json new file mode 100644 index 0000000..cc8fad8 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__service_unavailable_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 60, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/errors__validation_malformed_json.json b/tests/fixtures/parity/canonical/errors__validation_malformed_json.json new file mode 100644 index 0000000..9e1f37c --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__validation_malformed_json.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The request body is not valid JSON.", + "code": "malformed_body" + } +} diff --git a/tests/fixtures/parity/canonical/errors__validation_missing_field_hint.json b/tests/fixtures/parity/canonical/errors__validation_missing_field_hint.json new file mode 100644 index 0000000..2cf69f1 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__validation_missing_field_hint.json @@ -0,0 +1,29 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "text: Field required", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "hint": "Required field 'text' is missing. Unrecognised fields: content, bogus.", + "docs_url": "https://lenz.io/docs/errors#validation", + "unrecognized_fields": [ + "content", + "bogus" + ] + } +} diff --git a/tests/fixtures/parity/canonical/errors__validation_unknown_field_only.json b/tests/fixtures/parity/canonical/errors__validation_unknown_field_only.json new file mode 100644 index 0000000..877dab6 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__validation_unknown_field_only.json @@ -0,0 +1,28 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claim: Field required", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "hint": "Required field 'claim' is missing. Unrecognised fields: statement.", + "docs_url": "https://lenz.io/docs/errors#validation", + "unrecognized_fields": [ + "statement" + ] + } +} diff --git a/tests/fixtures/parity/canonical/errors__validation_unknown_field_wrong_type.json b/tests/fixtures/parity/canonical/errors__validation_unknown_field_wrong_type.json new file mode 100644 index 0000000..1aae12f --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__validation_unknown_field_wrong_type.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims: Input should be a valid list", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "claims" + ], + "msg": "Input should be a valid list", + "type": "list_type" + } + ], + "unrecognized_fields": [ + "bogus_field", + "another" + ] + } +} diff --git a/tests/fixtures/parity/canonical/errors__validation_wrong_type.json b/tests/fixtures/parity/canonical/errors__validation_wrong_type.json new file mode 100644 index 0000000..843bab6 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__validation_wrong_type.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "text: Input should be a valid string", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Input should be a valid string", + "type": "string_type" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/extract__401_no_key.json b/tests/fixtures/parity/canonical/extract__401_no_key.json new file mode 100644 index 0000000..1c3df51 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__401_no_key.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + } +} diff --git a/tests/fixtures/parity/canonical/extract__422_blank_text.json b/tests/fixtures/parity/canonical/extract__422_blank_text.json new file mode 100644 index 0000000..4242aac --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__422_blank_text.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Text is required.", + "code": "blank_input", + "errors": [ + { + "loc": [ + "body", + "text" + ], + "msg": "Text is required.", + "type": "blank_input" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/extract__422_focus_too_long.json b/tests/fixtures/parity/canonical/extract__422_focus_too_long.json new file mode 100644 index 0000000..6645b81 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__422_focus_too_long.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "focus must be at most 300 characters of plain text.", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "focus" + ], + "msg": "focus must be at most 300 characters of plain text.", + "type": "validation_error" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/extract__422_missing_text.json b/tests/fixtures/parity/canonical/extract__422_missing_text.json new file mode 100644 index 0000000..eb7b0fc --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__422_missing_text.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "text: Field required", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/extract__429_daily_limit.json b/tests/fixtures/parity/canonical/extract__429_daily_limit.json new file mode 100644 index 0000000..23ae05b --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__429_daily_limit.json @@ -0,0 +1,17 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "3600", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "code": "extract_daily_limit", + "limit": 1000, + "retry_after": 3600, + "docs_url": "https://lenz.io/docs/errors#rate-limits", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/canonical/extract__502_extraction_failed.json b/tests/fixtures/parity/canonical/extract__502_extraction_failed.json new file mode 100644 index 0000000..62f2e63 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__502_extraction_failed.json @@ -0,0 +1,12 @@ +{ + "status": 502, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Extraction failed", + "code": "extraction_failed" + } +} diff --git a/tests/fixtures/parity/canonical/extract__503_upstream_unavailable.json b/tests/fixtures/parity/canonical/extract__503_upstream_unavailable.json new file mode 100644 index 0000000..3315806 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__503_upstream_unavailable.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Our model providers are temporarily unavailable. Please retry shortly.", + "code": "upstream_unavailable", + "retry_after": 90, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/extract__focus_ready.json b/tests/fixtures/parity/canonical/extract__focus_ready.json new file mode 100644 index 0000000..29eccf2 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__focus_ready.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Beta fell 3% last year.", + "positions": null + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + } +} diff --git a/tests/fixtures/parity/canonical/extract__idempotency_body_mismatch_422.json b/tests/fixtures/parity/canonical/extract__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..e20089e --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__idempotency_body_mismatch_422.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + } +} diff --git a/tests/fixtures/parity/canonical/extract__idempotency_replay.json b/tests/fixtures/parity/canonical/extract__idempotency_replay.json new file mode 100644 index 0000000..b508889 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__idempotency_replay.json @@ -0,0 +1,34 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": null + }, + { + "claim": "Beta fell 3% last year.", + "positions": null + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + } +} diff --git a/tests/fixtures/parity/canonical/extract__locate_all_dropped.json b/tests/fixtures/parity/canonical/extract__locate_all_dropped.json new file mode 100644 index 0000000..7906518 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__locate_all_dropped.json @@ -0,0 +1,25 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "no_checkable_claim", + "claims": [], + "domain": "", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + } +} diff --git a/tests/fixtures/parity/canonical/extract__locate_failed.json b/tests/fixtures/parity/canonical/extract__locate_failed.json new file mode 100644 index 0000000..2fbe6a7 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__locate_failed.json @@ -0,0 +1,34 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": null + }, + { + "claim": "Beta fell 3% last year.", + "positions": null + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + } +} diff --git a/tests/fixtures/parity/canonical/extract__locate_leading_whitespace.json b/tests/fixtures/parity/canonical/extract__locate_leading_whitespace.json new file mode 100644 index 0000000..8033e2d --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__locate_leading_whitespace.json @@ -0,0 +1,36 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "start": 3, + "end": 25, + "text": "Alpha rose 5% in 2024." + } + ] + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + } +} diff --git a/tests/fixtures/parity/canonical/extract__locate_some_dropped.json b/tests/fixtures/parity/canonical/extract__locate_some_dropped.json new file mode 100644 index 0000000..11529fa --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__locate_some_dropped.json @@ -0,0 +1,46 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "start": 0, + "end": 22, + "text": "Alpha rose 5% in 2024." + } + ] + }, + { + "claim": "Beta fell 3% last year.", + "positions": [ + { + "start": 23, + "end": 46, + "text": "Beta fell 3% last year." + } + ] + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + } +} diff --git a/tests/fixtures/parity/canonical/extract__locate_true.json b/tests/fixtures/parity/canonical/extract__locate_true.json new file mode 100644 index 0000000..11529fa --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__locate_true.json @@ -0,0 +1,46 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "start": 0, + "end": 22, + "text": "Alpha rose 5% in 2024." + } + ] + }, + { + "claim": "Beta fell 3% last year.", + "positions": [ + { + "start": 23, + "end": 46, + "text": "Beta fell 3% last year." + } + ] + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + } +} diff --git a/tests/fixtures/parity/canonical/extract__no_match_with_focus.json b/tests/fixtures/parity/canonical/extract__no_match_with_focus.json new file mode 100644 index 0000000..eceaf0b --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__no_match_with_focus.json @@ -0,0 +1,16 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "no_match", + "claims": [], + "domain": "", + "key_entities": [], + "presumed_intent": "", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + } +} diff --git a/tests/fixtures/parity/canonical/extract__not_a_claim.json b/tests/fixtures/parity/canonical/extract__not_a_claim.json new file mode 100644 index 0000000..df06a66 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__not_a_claim.json @@ -0,0 +1,16 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "no_checkable_claim", + "claims": [], + "domain": "", + "key_entities": [], + "presumed_intent": "", + "original_input": "What a lovely day." + } +} diff --git a/tests/fixtures/parity/canonical/extract__not_a_claim_beside_claims.json b/tests/fixtures/parity/canonical/extract__not_a_claim_beside_claims.json new file mode 100644 index 0000000..646bbad --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__not_a_claim_beside_claims.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": null + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024." + } +} diff --git a/tests/fixtures/parity/canonical/extract__ready_one_claim.json b/tests/fixtures/parity/canonical/extract__ready_one_claim.json new file mode 100644 index 0000000..646bbad --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__ready_one_claim.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": null + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024." + } +} diff --git a/tests/fixtures/parity/canonical/extract__ready_several_claims.json b/tests/fixtures/parity/canonical/extract__ready_several_claims.json new file mode 100644 index 0000000..2fbe6a7 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__ready_several_claims.json @@ -0,0 +1,34 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": null + }, + { + "claim": "Beta fell 3% last year.", + "positions": null + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + } +} diff --git a/tests/fixtures/parity/canonical/extract__replay_later_200.json b/tests/fixtures/parity/canonical/extract__replay_later_200.json new file mode 100644 index 0000000..8fff1c2 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__replay_later_200.json @@ -0,0 +1,31 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": null + } +} diff --git a/tests/fixtures/parity/canonical/extract__url_fetch_502_unreadable.json b/tests/fixtures/parity/canonical/extract__url_fetch_502_unreadable.json new file mode 100644 index 0000000..62f2e63 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__url_fetch_502_unreadable.json @@ -0,0 +1,12 @@ +{ + "status": 502, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Extraction failed", + "code": "extraction_failed" + } +} diff --git a/tests/fixtures/parity/canonical/extract__url_fetch_503_upstream.json b/tests/fixtures/parity/canonical/extract__url_fetch_503_upstream.json new file mode 100644 index 0000000..14eded6 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__url_fetch_503_upstream.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The page could not be read right now. Please retry shortly.", + "code": "upstream_unavailable", + "retry_after": 90, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/extract__url_input_located.json b/tests/fixtures/parity/canonical/extract__url_input_located.json new file mode 100644 index 0000000..5c97376 --- /dev/null +++ b/tests/fixtures/parity/canonical/extract__url_input_located.json @@ -0,0 +1,36 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "start": null, + "end": null, + "text": "Alpha rose 5% in 2024." + } + ] + } + ], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "https://example.com/an-article" + } +} diff --git a/tests/fixtures/parity/canonical/review__401_no_credentials.json b/tests/fixtures/parity/canonical/review__401_no_credentials.json new file mode 100644 index 0000000..1c3df51 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__401_no_credentials.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + } +} diff --git a/tests/fixtures/parity/canonical/review__402_no_credits.json b/tests/fixtures/parity/canonical/review__402_no_credits.json new file mode 100644 index 0000000..5a9f5be --- /dev/null +++ b/tests/fixtures/parity/canonical/review__402_no_credits.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining credits to assess the draft.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 100, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 100, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/canonical/review__402_no_credits_exhausted.json b/tests/fixtures/parity/canonical/review__402_no_credits_exhausted.json new file mode 100644 index 0000000..2ec177c --- /dev/null +++ b/tests/fixtures/parity/canonical/review__402_no_credits_exhausted.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining credits to assess the draft.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/canonical/review__422_blank_text.json b/tests/fixtures/parity/canonical/review__422_blank_text.json new file mode 100644 index 0000000..ee12b17 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_blank_text.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "text: send the draft, or one public http(s) URL.", + "code": "blank_input", + "errors": [ + { + "loc": [ + "body", + "text" + ], + "msg": "text: send the draft, or one public http(s) URL.", + "type": "blank_input" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_citations_options_object.json b/tests/fixtures/parity/canonical/review__422_citations_options_object.json new file mode 100644 index 0000000..8932e84 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_citations_options_object.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "citations: Extra inputs are not permitted", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "citations" + ], + "msg": "Extra inputs are not permitted", + "type": "extra_forbidden" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_depth_unknown.json b/tests/fixtures/parity/canonical/review__422_depth_unknown.json new file mode 100644 index 0000000..3ef5a12 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_depth_unknown.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "escalate.depth: Input should be 'standard' or 'low'", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "depth" + ], + "msg": "Input should be 'standard' or 'low'", + "type": "literal_error", + "ctx": { + "expected": "'standard' or 'low'" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_invalid_confidence_band.json b/tests/fixtures/parity/canonical/review__422_invalid_confidence_band.json new file mode 100644 index 0000000..9ece217 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_invalid_confidence_band.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "escalate.confidence: Unknown confidence band 'certain'; use low, medium or high.", + "code": "invalid_confidence_band", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "confidence" + ], + "msg": "Unknown confidence band 'certain'; use low, medium or high.", + "type": "invalid_confidence_band", + "ctx": { + "band": "certain" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_invalid_verdict_label.json b/tests/fixtures/parity/canonical/review__422_invalid_verdict_label.json new file mode 100644 index 0000000..ea83a57 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_invalid_verdict_label.json @@ -0,0 +1,28 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "escalate.verdicts: Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True.", + "code": "invalid_verdict_label", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "verdicts" + ], + "msg": "Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True.", + "type": "invalid_verdict_label", + "ctx": { + "label": "Misleading", + "labels": "False, Mostly False, Mixed, Mostly True, True" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_max_assessments_negative.json b/tests/fixtures/parity/canonical/review__422_max_assessments_negative.json new file mode 100644 index 0000000..0bb1242 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_max_assessments_negative.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "escalate.max_assessments: Input should be greater than or equal to 0", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_assessments" + ], + "msg": "Input should be greater than or equal to 0", + "type": "greater_than_equal", + "ctx": { + "ge": 0 + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_max_citations_negative.json b/tests/fixtures/parity/canonical/review__422_max_citations_negative.json new file mode 100644 index 0000000..dd2cde4 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_max_citations_negative.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "escalate.max_citations: Input should be greater than or equal to 0", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_citations" + ], + "msg": "Input should be greater than or equal to 0", + "type": "greater_than_equal", + "ctx": { + "ge": 0 + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_max_citations_too_big.json b/tests/fixtures/parity/canonical/review__422_max_citations_too_big.json new file mode 100644 index 0000000..3d8860c --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_max_citations_too_big.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "escalate.max_citations: Input should be less than or equal to 20", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_citations" + ], + "msg": "Input should be less than or equal to 20", + "type": "less_than_equal", + "ctx": { + "le": 20 + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_max_verifications_too_big.json b/tests/fixtures/parity/canonical/review__422_max_verifications_too_big.json new file mode 100644 index 0000000..cc5e5b1 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_max_verifications_too_big.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "escalate.max_verifications: Input should be less than or equal to 20", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_verifications" + ], + "msg": "Input should be less than or equal to 20", + "type": "less_than_equal", + "ctx": { + "le": 20 + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_text_missing.json b/tests/fixtures/parity/canonical/review__422_text_missing.json new file mode 100644 index 0000000..eb7b0fc --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_text_missing.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "text: Field required", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_text_wrong_type.json b/tests/fixtures/parity/canonical/review__422_text_wrong_type.json new file mode 100644 index 0000000..843bab6 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_text_wrong_type.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "text: Input should be a valid string", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Input should be a valid string", + "type": "string_type" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_unknown_escalate_field.json b/tests/fixtures/parity/canonical/review__422_unknown_escalate_field.json new file mode 100644 index 0000000..74bc52a --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_unknown_escalate_field.json @@ -0,0 +1,24 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "escalate.nope: Extra inputs are not permitted", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "nope" + ], + "msg": "Extra inputs are not permitted", + "type": "extra_forbidden" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_unknown_top_level_field.json b/tests/fixtures/parity/canonical/review__422_unknown_top_level_field.json new file mode 100644 index 0000000..eecb052 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_unknown_top_level_field.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "max_verifications: Extra inputs are not permitted", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "max_verifications" + ], + "msg": "Extra inputs are not permitted", + "type": "extra_forbidden" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_unsupported_language.json b/tests/fixtures/parity/canonical/review__422_unsupported_language.json new file mode 100644 index 0000000..9382428 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_unsupported_language.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "code": "unsupported_language", + "errors": [ + { + "loc": [ + "body", + "language" + ], + "msg": "Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "type": "unsupported_language" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__422_visibility_public.json b/tests/fixtures/parity/canonical/review__422_visibility_public.json new file mode 100644 index 0000000..31600c2 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_visibility_public.json @@ -0,0 +1,26 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "visibility: Input should be 'private' or 'unlisted'", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "visibility" + ], + "msg": "Input should be 'private' or 'unlisted'", + "type": "literal_error", + "ctx": { + "expected": "'private' or 'unlisted'" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__429_extract_daily_limit.json b/tests/fixtures/parity/canonical/review__429_extract_daily_limit.json new file mode 100644 index 0000000..320069f --- /dev/null +++ b/tests/fixtures/parity/canonical/review__429_extract_daily_limit.json @@ -0,0 +1,17 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Daily /extract limit of 1000 reached for this account. A review of a URL reads the page like /extract and counts as one /extract call.", + "code": "extract_daily_limit", + "limit": 1000, + "retry_after": 60, + "docs_url": "https://lenz.io/docs/errors#rate-limits", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/canonical/review__429_review_in_flight.json b/tests/fixtures/parity/canonical/review__429_review_in_flight.json new file mode 100644 index 0000000..61295d1 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__429_review_in_flight.json @@ -0,0 +1,14 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This account already has 3 reviews running. Retry when one completes.", + "code": "review_in_flight", + "retry_after": 60 + } +} diff --git a/tests/fixtures/parity/canonical/review__503_capacity.json b/tests/fixtures/parity/canonical/review__503_capacity.json new file mode 100644 index 0000000..bcff7db --- /dev/null +++ b/tests/fixtures/parity/canonical/review__503_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 90, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/review__503_citations_capacity.json b/tests/fixtures/parity/canonical/review__503_citations_capacity.json new file mode 100644 index 0000000..bcff7db --- /dev/null +++ b/tests/fixtures/parity/canonical/review__503_citations_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 90, + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/review__get_403_insufficient_scope.json b/tests/fixtures/parity/canonical/review__get_403_insufficient_scope.json new file mode 100644 index 0000000..80c529c --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_403_insufficient_scope.json @@ -0,0 +1,14 @@ +{ + "status": 403, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"history:read\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "history:read" + } +} diff --git a/tests/fixtures/parity/canonical/review__get_404_not_found.json b/tests/fixtures/parity/canonical/review__get_404_not_found.json new file mode 100644 index 0000000..8045cd8 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_404_not_found.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Review not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/review__get_404_other_account.json b/tests/fixtures/parity/canonical/review__get_404_other_account.json new file mode 100644 index 0000000..8045cd8 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_404_other_account.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Review not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/review__get_410_purged.json b/tests/fixtures/parity/canonical/review__get_410_purged.json new file mode 100644 index 0000000..bedf6a5 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_410_purged.json @@ -0,0 +1,13 @@ +{ + "status": 410, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This review is no longer available: its account removes content after a set period.", + "code": "purged", + "purged_at": "2026-10-01T09:07:00.123456Z" + } +} diff --git a/tests/fixtures/parity/canonical/review__get_422_unknown_view.json b/tests/fixtures/parity/canonical/review__get_422_unknown_view.json new file mode 100644 index 0000000..a9df405 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_422_unknown_view.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "view: use full or issues.", + "code": "validation_error", + "errors": [ + { + "loc": [ + "query", + "view" + ], + "msg": "view: use full or issues.", + "type": "validation_error" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_assessing.json b/tests/fixtures/parity/canonical/review__get_assessing.json new file mode 100644 index 0000000..1b8bc1d --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_assessing.json @@ -0,0 +1,107 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "assessing", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 0 + }, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": 10, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": null, + "assessment": { + "status": "running", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": null, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": null, + "assessment": { + "status": "running", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": null, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_assessment_rows_full_fields.json b/tests/fixtures/parity/canonical/review__get_assessment_rows_full_fields.json new file mode 100644 index 0000000..8680925 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_assessment_rows_full_fields.json @@ -0,0 +1,169 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 0, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 1 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": null, + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "cap" + }, + "key_finding": null, + "rationale": "Two reviewers found the date wrong.", + "suggested_rewrite": "The EU AI Act entered into force in August 2024.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [ + { + "claim_index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "stage": "assessment", + "failure": { + "code": "timeout", + "detail": "The check did not finish inside its time budget.", + "hint": "The check ran out of time. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": "Two reviewers found the date wrong.", + "dissent": "One reviewer read the claim as about the entry into force.", + "suggested_rewrite": "The EU AI Act entered into force in August 2024.", + "more_claims": [ + "The EU AI Act took effect in 2024.", + "It took effect in March." + ], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "cap" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": null, + "assessment": { + "status": "failed", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": { + "code": "timeout", + "detail": "The check did not finish inside its time budget.", + "hint": "The check ran out of time. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": null + } + }, + "escalation": null, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_clean.json b/tests/fixtures/parity/canonical/review__get_citations_clean.json new file mode 100644 index 0000000..332658b --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_clean.json @@ -0,0 +1,182 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_failed_and_unchecked.json b/tests/fixtures/parity/canonical/review__get_citations_failed_and_unchecked.json new file mode 100644 index 0000000..b180507 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_failed_and_unchecked.json @@ -0,0 +1,200 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 1, + "failed": 1 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "code": "internal", + "detail": "The check failed on our side.", + "hint": "The check failed on our side. Try again later.", + "failure_class": "internal", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#internal" + } + } + ], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "The check failed on our side. Try again later.", + "failure": { + "code": "internal", + "detail": "The check failed on our side.", + "hint": "The check failed on our side. Try again later.", + "failure_class": "internal", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#internal" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "unchecked", + "source": null, + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "unchecked", + "snippet": null, + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": "no_text", + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_issue_full.json b/tests/fixtures/parity/canonical/review__get_citations_issue_full.json new file mode 100644 index 0000000..b3243d6 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_issue_full.json @@ -0,0 +1,204 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 1, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "finding": "contradicted", + "source": "support", + "snippet": "The source says so.", + "rationale": "The page states it.", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "failure": null + } + ], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "contradicted", + "source": "support", + "is_issue": true + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "contradicted", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_issue_issues_view.json b/tests/fixtures/parity/canonical/review__get_citations_issue_issues_view.json new file mode 100644 index 0000000..9f60f10 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_issue_issues_view.json @@ -0,0 +1,94 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "issues", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 1, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "finding": "contradicted", + "source": "support", + "snippet": "The source says so.", + "rationale": "The page states it.", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "failure": null + } + ], + "citation_failures": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_limit_exactly_at_limit.json b/tests/fixtures/parity/canonical/review__get_citations_limit_exactly_at_limit.json new file mode 100644 index 0000000..a81b16f --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_limit_exactly_at_limit.json @@ -0,0 +1,182 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 2, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_limit_reached.json b/tests/fixtures/parity/canonical/review__get_citations_limit_reached.json new file mode 100644 index 0000000..33eeb1b --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_limit_reached.json @@ -0,0 +1,156 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 1, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 1, + "citation_limit": 1, + "citation_limit_exceeded": true, + "citation_checks": { + "checked": 1, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [ + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "sentence": "The agency said so in a statement.", + "position": { + "start": 111, + "end": 178, + "text": null + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_no_claim_but_citations.json b/tests/fixtures/parity/canonical/review__get_citations_no_claim_but_citations.json new file mode 100644 index 0000000..fbb5a29 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_no_claim_but_citations.json @@ -0,0 +1,153 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": 0, + "claims_selected": 0, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_no_claim_no_citation.json b/tests/fixtures/parity/canonical/review__get_citations_no_claim_no_citation.json new file mode 100644 index 0000000..e9cdd01 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_no_claim_no_citation.json @@ -0,0 +1,70 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": null, + "claims_selected": null, + "claim_limit": 20, + "claim_limit_exceeded": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_not_asked.json b/tests/fixtures/parity/canonical/review__get_citations_not_asked.json new file mode 100644 index 0000000..27d5c36 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_not_asked.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_only.json b/tests/fixtures/parity/canonical/review__get_citations_only.json new file mode 100644 index 0000000..4a86304 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_only.json @@ -0,0 +1,160 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 0, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 0, + "claim_limit": 0, + "claim_limit_exceeded": true, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [ + "The EU AI Act took effect in March 2024." + ], + "more_claim_locations": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "positions": null + } + ], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_partly_supported.json b/tests/fixtures/parity/canonical/review__get_citations_partly_supported.json new file mode 100644 index 0000000..44488cd --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_partly_supported.json @@ -0,0 +1,182 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "partly_supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "partly_supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_running.json b/tests/fixtures/parity/canonical/review__get_citations_running.json new file mode 100644 index 0000000..ff45b91 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_running.json @@ -0,0 +1,166 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "verifying", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": 15, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": null, + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": null, + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_skipped_switched_off.json b/tests/fixtures/parity/canonical/review__get_citations_skipped_switched_off.json new file mode 100644 index 0000000..acfa95b --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_skipped_switched_off.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": "switched_off" + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_citations_skipped_url_input.json b/tests/fixtures/parity/canonical/review__get_citations_skipped_url_input.json new file mode 100644 index 0000000..5919306 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_citations_skipped_url_input.json @@ -0,0 +1,70 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_found": null, + "claims_selected": null, + "claim_limit": 20, + "claim_limit_exceeded": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": "url_input" + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": null, + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/canonical/review__get_claim_limit_not_reached.json b/tests/fixtures/parity/canonical/review__get_claim_limit_not_reached.json new file mode 100644 index 0000000..548c3c8 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_claim_limit_not_reached.json @@ -0,0 +1,127 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 3, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 3, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_claim_limit_reached_exact.json b/tests/fixtures/parity/canonical/review__get_claim_limit_reached_exact.json new file mode 100644 index 0000000..6432f12 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_claim_limit_reached_exact.json @@ -0,0 +1,127 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 2, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 2, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_completed_assess_only.json b/tests/fixtures/parity/canonical/review__get_completed_assess_only.json new file mode 100644 index 0000000..107ee44 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_completed_assess_only.json @@ -0,0 +1,153 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 0, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": null, + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "cap" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "cap" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_completed_clean.json b/tests/fixtures/parity/canonical/review__get_completed_clean.json new file mode 100644 index 0000000..0849314 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_completed_clean.json @@ -0,0 +1,127 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_completed_issues_all_verified.json b/tests/fixtures/parity/canonical/review__get_completed_issues_all_verified.json new file mode 100644 index 0000000..1b82d0e --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_completed_issues_all_verified.json @@ -0,0 +1,256 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 3, + "claims_selected": 3, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 3, + "failed": 0 + }, + "verifications": { + "planned": 2, + "completed": 2, + "failed": 0 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 23 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "failure": null, + "suggested_edits": null + }, + { + "claim_index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b79019e4", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", + "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "Mostly False", + "confidence": "medium", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "c010dca9a43543f518e49f4ec7842ceb", + "verification_id": "b4e2e99d", + "claim": "The EU AI Act took effect in March 2024.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "executive_summary": "Summary for The EU AI Act took effect in March 2024.", + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:49:00.123456+00:00", + "completed_at": "2026-10-01T09:56:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "failure": null + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "4e02e88b137711f18ba7758af5c40837", + "verification_id": "b79019e4", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:49:00.123456+00:00", + "completed_at": "2026-10-01T09:56:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", + "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "failure": null + }, + "suggested_edits": null + }, + { + "index": 2, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_completed_issues_full.json b/tests/fixtures/parity/canonical/review__get_completed_issues_full.json new file mode 100644 index 0000000..ff3d65d --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_completed_issues_full.json @@ -0,0 +1,309 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 4, + "claims_selected": 4, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 3, + "failed": 1 + }, + "verifications": { + "planned": 2, + "completed": 1, + "failed": 1 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 13 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "failure": null, + "suggested_edits": null + }, + { + "claim_index": 2, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "code": "research_empty", + "detail": "No sources about the claim were found.", + "hint": null, + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + }, + "suggested_edits": null + } + ], + "failures": [ + { + "claim_index": 1, + "claim": "Water boils at 100 C at sea level.", + "stage": "assessment", + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } + ], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "Mostly False", + "confidence": "medium", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "7df4009bc27aab64ce5d5a01fbafe599", + "verification_id": "b4e2e99d", + "claim": "The EU AI Act took effect in March 2024.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "executive_summary": "Summary for The EU AI Act took effect in March 2024.", + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:42:00.123456+00:00", + "completed_at": "2026-10-01T09:49:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "failure": null + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": null, + "assessment": { + "status": "failed", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + "escalation": null, + "verification": null, + "suggested_edits": null + }, + { + "index": 2, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "verification_id": null, + "task_id": "4634b89821fd877f1edf8aaeb82b7ee5", + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "completed_at": null, + "verification_url": null, + "url": null, + "failure": { + "code": "research_empty", + "detail": "No sources about the claim were found.", + "hint": null, + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + }, + "status": "failed", + "content_status": "available" + }, + "suggested_edits": null + }, + { + "index": 3, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_completed_issues_view_issues.json b/tests/fixtures/parity/canonical/review__get_completed_issues_view_issues.json new file mode 100644 index 0000000..a136287 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_completed_issues_view_issues.json @@ -0,0 +1,136 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "issues", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 4, + "claims_selected": 4, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 3, + "failed": 1 + }, + "verifications": { + "planned": 2, + "completed": 1, + "failed": 1 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 13 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "failure": null, + "suggested_edits": null + }, + { + "claim_index": 2, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "code": "research_empty", + "detail": "No sources about the claim were found.", + "hint": null, + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + }, + "suggested_edits": null + } + ], + "failures": [ + { + "claim_index": 1, + "claim": "Water boils at 100 C at sea level.", + "stage": "assessment", + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_completed_positions.json b/tests/fixtures/parity/canonical/review__get_completed_positions.json new file mode 100644 index 0000000..7a3c9e1 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_completed_positions.json @@ -0,0 +1,139 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": 0, + "end": 40, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": [ + { + "start": 55, + "end": 89, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_completed_recovered_claim.json b/tests/fixtures/parity/canonical/review__get_completed_recovered_claim.json new file mode 100644 index 0000000..aafdbe0 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_completed_recovered_claim.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "Orbit Motors stock increased by 40% in 2022.", + "positions": [], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_completed_with_untraced_claim.json b/tests/fixtures/parity/canonical/review__get_completed_with_untraced_claim.json new file mode 100644 index 0000000..64ad9f0 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_completed_with_untraced_claim.json @@ -0,0 +1,139 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": 0, + "end": 40, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": [ + { + "start": 50, + "end": 84, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_during_quick_check.json b/tests/fixtures/parity/canonical/review__get_during_quick_check.json new file mode 100644 index 0000000..06f27e1 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_during_quick_check.json @@ -0,0 +1,87 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "assessing", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 0 + }, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": 10, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": null, + "assessment": { + "status": "running", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": null, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_failed_every_assessment_failed.json b/tests/fixtures/parity/canonical/review__get_failed_every_assessment_failed.json new file mode 100644 index 0000000..93ea710 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_failed_every_assessment_failed.json @@ -0,0 +1,159 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 2 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "stage": "assessment", + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + { + "claim_index": 1, + "claim": "Water boils at 100 C at sea level.", + "stage": "assessment", + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } + ], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": null, + "assessment": { + "status": "failed", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + "escalation": null, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": null, + "assessment": { + "status": "failed", + "verdict": null, + "confidence": null, + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": { + "code": "upstream_unavailable", + "detail": "A service the check depends on was unavailable.", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + "escalation": null, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": { + "code": "assessment_failed", + "detail": "No claim could be assessed.", + "hint": "Retry the review with a new Idempotency-Key; nothing was charged.", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_failed_no_claim.json b/tests/fixtures/parity/canonical/review__get_failed_no_claim.json new file mode 100644 index 0000000..d8506ac --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_failed_no_claim.json @@ -0,0 +1,70 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": null, + "claims_selected": null, + "claim_limit": 20, + "claim_limit_exceeded": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "The text is a greeting, not a statement about the world. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/canonical/review__get_failed_no_claim_untraced.json b/tests/fixtures/parity/canonical/review__get_failed_no_claim_untraced.json new file mode 100644 index 0000000..9d2e2ea --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_failed_no_claim_untraced.json @@ -0,0 +1,70 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": null, + "claims_selected": null, + "claim_limit": 20, + "claim_limit_exceeded": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "None of the claims found in the text could be traced back to it. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/canonical/review__get_more_claims.json b/tests/fixtures/parity/canonical/review__get_more_claims.json new file mode 100644 index 0000000..ffaacb2 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_more_claims.json @@ -0,0 +1,152 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 2, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 3, + "claims_selected": 2, + "claim_limit": 2, + "claim_limit_exceeded": true, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": 0, + "end": 40, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": [ + { + "start": 50, + "end": 84, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [ + "The Eiffel Tower is in Berlin." + ], + "more_claim_locations": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "positions": [ + { + "start": 91, + "end": 121, + "text": "The Eiffel Tower is in Berlin." + } + ] + } + ], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..62d9316 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,150 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 1, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 11 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "7df4009bc27aab64ce5d5a01fbafe599", + "verification_id": "b4e2e99d", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T23:55:00.123456+00:00", + "completed_at": "2026-10-02T00:03:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "failure": null + }, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..1aeb435 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_same_day_hours_apart.json @@ -0,0 +1,150 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 1, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 11 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "7df4009bc27aab64ce5d5a01fbafe599", + "verification_id": "b4e2e99d", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T08:00:00.123456+00:00", + "completed_at": "2026-10-01T20:00:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "failure": null + }, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_policy_resolved.json b/tests/fixtures/parity/canonical/review__get_policy_resolved.json new file mode 100644 index 0000000..cc1e568 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_policy_resolved.json @@ -0,0 +1,97 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 2, + "depth": "low", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_queued.json b/tests/fixtures/parity/canonical/review__get_queued.json new file mode 100644 index 0000000..1b08c33 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_queued.json @@ -0,0 +1,63 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "queued", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": null, + "claims_selected": null, + "claim_limit": 20, + "claim_limit_exceeded": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": 10, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": null, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/canonical/review__get_suggested_edits.json b/tests/fixtures/parity/canonical/review__get_suggested_edits.json new file mode 100644 index 0000000..8f4e0a0 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_suggested_edits.json @@ -0,0 +1,266 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "verifying", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "suggest_edits": true, + "max_citations": 0 + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 2, + "completed": 1, + "failed": 0 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 12 + }, + "poll_after_seconds": 15, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "7c83247c", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": { + "status": "completed", + "edits": [ + { + "position": 0, + "start": 23, + "end": 29, + "text": "Berlin", + "replacement": "Paris" + } + ] + } + }, + { + "claim_index": 1, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": null, + "suggested_edits": { + "status": "pending", + "edits": null + } + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": [ + { + "start": 0, + "end": 30, + "text": "The Eiffel Tower is in Berlin." + } + ], + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "9bca1e0819de60a750186e812dc58e2b", + "verification_id": "7c83247c", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:35:00.123456+00:00", + "completed_at": "2026-10-01T09:42:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "failure": null + }, + "suggested_edits": { + "status": "completed", + "edits": [ + { + "position": 0, + "start": 23, + "end": 29, + "text": "Berlin", + "replacement": "Paris" + } + ] + } + }, + { + "index": 1, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": 45, + "end": 85, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "processing", + "content_status": "available", + "verification_id": null, + "task_id": null, + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "completed_at": null, + "verification_url": null, + "url": null, + "failure": null + }, + "suggested_edits": { + "status": "pending", + "edits": null + } + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_suggested_edits_issues_view.json b/tests/fixtures/parity/canonical/review__get_suggested_edits_issues_view.json new file mode 100644 index 0000000..8914521 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_suggested_edits_issues_view.json @@ -0,0 +1,129 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "issues", + "status": "verifying", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "suggest_edits": true, + "max_citations": 0 + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 2, + "completed": 1, + "failed": 0 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 12 + }, + "poll_after_seconds": 15, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "7c83247c", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": { + "status": "completed", + "edits": [ + { + "position": 0, + "start": 23, + "end": 29, + "text": "Berlin", + "replacement": "Paris" + } + ] + } + }, + { + "claim_index": 1, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": null, + "suggested_edits": { + "status": "pending", + "edits": null + } + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_url_review.json b/tests/fixtures/parity/canonical/review__get_url_review.json new file mode 100644 index 0000000..90ea98e --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_url_review.json @@ -0,0 +1,105 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": null, + "end": null, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__get_verifying.json b/tests/fixtures/parity/canonical/review__get_verifying.json new file mode 100644 index 0000000..1663113 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_verifying.json @@ -0,0 +1,176 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "verifying", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 0, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": 15, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "processing", + "content_status": "available", + "verification_id": null, + "task_id": null, + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "completed_at": null, + "verification_url": null, + "url": null, + "failure": null + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/canonical/review__idempotency_body_mismatch_422.json b/tests/fixtures/parity/canonical/review__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..e20089e --- /dev/null +++ b/tests/fixtures/parity/canonical/review__idempotency_body_mismatch_422.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + } +} diff --git a/tests/fixtures/parity/canonical/review__idempotency_conflict_409.json b/tests/fixtures/parity/canonical/review__idempotency_conflict_409.json new file mode 100644 index 0000000..8bbdff7 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__idempotency_conflict_409.json @@ -0,0 +1,13 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A review with this Idempotency-Key is still being created. Retry shortly.", + "code": "idempotency_conflict", + "review_id": null + } +} diff --git a/tests/fixtures/parity/canonical/review__idempotency_conflict_409_existing_review.json b/tests/fixtures/parity/canonical/review__idempotency_conflict_409_existing_review.json new file mode 100644 index 0000000..edef440 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__idempotency_conflict_409_existing_review.json @@ -0,0 +1,13 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A review with this Idempotency-Key is still being created. Retry shortly.", + "code": "idempotency_conflict", + "review_id": "d6b2bd72" + } +} diff --git a/tests/fixtures/parity/canonical/review__idempotent_replay_202.json b/tests/fixtures/parity/canonical/review__idempotent_replay_202.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/canonical/review__idempotent_replay_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/review__receipt_202.json b/tests/fixtures/parity/canonical/review__receipt_202.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/canonical/review__receipt_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/review__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/canonical/review__receipt_202_empty_webhook_url.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/canonical/review__receipt_202_empty_webhook_url.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/review__receipt_202_webhook_url.json b/tests/fixtures/parity/canonical/review__receipt_202_webhook_url.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/canonical/review__receipt_202_webhook_url.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/review__replay_later_202.json b/tests/fixtures/parity/canonical/review__replay_later_202.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/canonical/review__replay_later_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/verify__batch_202.json b/tests/fixtures/parity/canonical/verify__batch_202.json new file mode 100644 index 0000000..be544f2 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__batch_202.json @@ -0,0 +1,21 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "claim": "The Earth is round." + }, + { + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "claim": "Water boils at 100C at sea level." + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__batch_capacity_503.json b/tests/fixtures/parity/canonical/verify__batch_capacity_503.json new file mode 100644 index 0000000..2e25ef0 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__batch_capacity_503.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": "", + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/verify__batch_empty_422.json b/tests/fixtures/parity/canonical/verify__batch_empty_422.json new file mode 100644 index 0000000..0082c60 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__batch_empty_422.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims is required and must be non-empty.", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "claims" + ], + "msg": "claims is required and must be non-empty.", + "type": "validation_error" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__batch_idempotency_conflict_409.json b/tests/fixtures/parity/canonical/verify__batch_idempotency_conflict_409.json new file mode 100644 index 0000000..788b394 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__batch_idempotency_conflict_409.json @@ -0,0 +1,13 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A request with this Idempotency-Key is already in progress.", + "code": "idempotency_conflict", + "task_id": "7cff18d" + } +} diff --git a/tests/fixtures/parity/canonical/verify__batch_item_missing_claim_422.json b/tests/fixtures/parity/canonical/verify__batch_item_missing_claim_422.json new file mode 100644 index 0000000..7017bcc --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__batch_item_missing_claim_422.json @@ -0,0 +1,25 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims[1].claim: Field required", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "claims", + 1, + "text" + ], + "msg": "Field required", + "type": "missing" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__batch_no_credits_402.json b/tests/fixtures/parity/canonical/verify__batch_no_credits_402.json new file mode 100644 index 0000000..8991dc1 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__batch_no_credits_402.json @@ -0,0 +1,20 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Insufficient credits for batch.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "requested": 1, + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 10 + } +} diff --git a/tests/fixtures/parity/canonical/verify__batch_partial_202.json b/tests/fixtures/parity/canonical/verify__batch_partial_202.json new file mode 100644 index 0000000..1e3656f --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__batch_partial_202.json @@ -0,0 +1,18 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "claim": "First claim." + } + ], + "partial": true + } +} diff --git a/tests/fixtures/parity/canonical/verify__batch_too_many_422.json b/tests/fixtures/parity/canonical/verify__batch_too_many_422.json new file mode 100644 index 0000000..5105df7 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__batch_too_many_422.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Batch size exceeds maximum of 20.", + "code": "too_many_items", + "errors": [ + { + "loc": [ + "body", + "claims" + ], + "msg": "Batch size exceeds maximum of 20.", + "type": "too_many_items" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__blank_claim_422.json b/tests/fixtures/parity/canonical/verify__blank_claim_422.json new file mode 100644 index 0000000..a5a1062 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__blank_claim_422.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claim is required.", + "code": "blank_input", + "errors": [ + { + "loc": [ + "body", + "claim" + ], + "msg": "claim is required.", + "type": "blank_input" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__capacity_503.json b/tests/fixtures/parity/canonical/verify__capacity_503.json new file mode 100644 index 0000000..2e25ef0 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__capacity_503.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": "", + "docs_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/canonical/verify__idempotency_body_mismatch_422.json b/tests/fixtures/parity/canonical/verify__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..e20089e --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__idempotency_body_mismatch_422.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + } +} diff --git a/tests/fixtures/parity/canonical/verify__idempotency_conflict_409.json b/tests/fixtures/parity/canonical/verify__idempotency_conflict_409.json new file mode 100644 index 0000000..1f8ddf5 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__idempotency_conflict_409.json @@ -0,0 +1,13 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A request with this Idempotency-Key is already in progress.", + "code": "idempotency_conflict", + "task_id": "7cff18da2d979d6370e54a160be121ad" + } +} diff --git a/tests/fixtures/parity/canonical/verify__idempotency_key_replay.json b/tests/fixtures/parity/canonical/verify__idempotency_key_replay.json new file mode 100644 index 0000000..b510371 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__idempotency_key_replay.json @@ -0,0 +1,12 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/verify__implicit_dedup_200.json b/tests/fixtures/parity/canonical/verify__implicit_dedup_200.json new file mode 100644 index 0000000..b510371 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__implicit_dedup_200.json @@ -0,0 +1,12 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/verify__implicit_repeat_replay.json b/tests/fixtures/parity/canonical/verify__implicit_repeat_replay.json new file mode 100644 index 0000000..b510371 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__implicit_repeat_replay.json @@ -0,0 +1,12 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/verify__invalid_depth_422.json b/tests/fixtures/parity/canonical/verify__invalid_depth_422.json new file mode 100644 index 0000000..ae4883c --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__invalid_depth_422.json @@ -0,0 +1,26 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "depth: Input should be 'standard' or 'low'", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "depth" + ], + "msg": "Input should be 'standard' or 'low'", + "type": "literal_error", + "ctx": { + "expected": "'standard' or 'low'" + } + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__invalid_language_422.json b/tests/fixtures/parity/canonical/verify__invalid_language_422.json new file mode 100644 index 0000000..5e82c5f --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__invalid_language_422.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "code": "unsupported_language", + "errors": [ + { + "loc": [ + "body", + "language" + ], + "msg": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "type": "unsupported_language" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__list_200.json b/tests/fixtures/parity/canonical/verify__list_200.json new file mode 100644 index 0000000..0b7e73b --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__list_200.json @@ -0,0 +1,45 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "language": "en" + }, + { + "verification_id": "b4e2e99d", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:35:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "language": "en" + } + ], + "total": 2, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/verify__list_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/verify__list_200_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..267a53e --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__list_200_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "completed_at": "2026-10-02T00:03:00.123456+00:00", + "language": "en" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/verify__list_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/verify__list_200_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..65e23bc --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__list_200_modified_at_same_day_hours_apart.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "completed_at": "2026-10-01T20:00:00.123456+00:00", + "language": "en" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/verify__list_clamped_200.json b/tests/fixtures/parity/canonical/verify__list_clamped_200.json new file mode 100644 index 0000000..3d639d4 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__list_clamped_200.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 0, + "page": 1, + "page_size": 100 + } +} diff --git a/tests/fixtures/parity/canonical/verify__list_empty_200.json b/tests/fixtures/parity/canonical/verify__list_empty_200.json new file mode 100644 index 0000000..b419316 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__list_empty_200.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 0, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/canonical/verify__list_page2_200.json b/tests/fixtures/parity/canonical/verify__list_page2_200.json new file mode 100644 index 0000000..f83290d --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__list_page2_200.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "language": "en" + } + ], + "total": 3, + "page": 2, + "page_size": 2 + } +} diff --git a/tests/fixtures/parity/canonical/verify__misnamed_field_422.json b/tests/fixtures/parity/canonical/verify__misnamed_field_422.json new file mode 100644 index 0000000..877dab6 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__misnamed_field_422.json @@ -0,0 +1,28 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claim: Field required", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "hint": "Required field 'claim' is missing. Unrecognised fields: statement.", + "docs_url": "https://lenz.io/docs/errors#validation", + "unrecognized_fields": [ + "statement" + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__missing_claim_422.json b/tests/fixtures/parity/canonical/verify__missing_claim_422.json new file mode 100644 index 0000000..9136471 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__missing_claim_422.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claim: Field required", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__no_credits_402.json b/tests/fixtures/parity/canonical/verify__no_credits_402.json new file mode 100644 index 0000000..1f73a67 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__no_credits_402.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining claim checks.", + "code": "no_credits", + "docs_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 10 + } +} diff --git a/tests/fixtures/parity/canonical/verify__replay_later_202.json b/tests/fixtures/parity/canonical/verify__replay_later_202.json new file mode 100644 index 0000000..b6dbf5e --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__replay_later_202.json @@ -0,0 +1,13 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "78ac4e9f234ea3bd" + } +} diff --git a/tests/fixtures/parity/canonical/verify__select_202.json b/tests/fixtures/parity/canonical/verify__select_202.json new file mode 100644 index 0000000..be544f2 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__select_202.json @@ -0,0 +1,21 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "claim": "The Earth is round." + }, + { + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "claim": "Water boils at 100C at sea level." + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__select_empty_422.json b/tests/fixtures/parity/canonical/verify__select_empty_422.json new file mode 100644 index 0000000..8651afb --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__select_empty_422.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims is required.", + "code": "blank_input", + "errors": [ + { + "loc": [ + "body", + "claims" + ], + "msg": "claims is required.", + "type": "blank_input" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__select_invalid_selection_422.json b/tests/fixtures/parity/canonical/verify__select_invalid_selection_422.json new file mode 100644 index 0000000..d8f6dd6 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__select_invalid_selection_422.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Selected text was not one of the offered claims: 'The Moon is made of cheese.'", + "code": "invalid_selection", + "errors": [ + { + "loc": [ + "body", + "claims" + ], + "msg": "Selected text was not one of the offered claims: 'The Moon is made of cheese.'", + "type": "invalid_selection" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__select_no_selection_pending_409.json b/tests/fixtures/parity/canonical/verify__select_no_selection_pending_409.json new file mode 100644 index 0000000..149402e --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__select_no_selection_pending_409.json @@ -0,0 +1,12 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This task has no pending claim selection.", + "code": "no_selection_pending" + } +} diff --git a/tests/fixtures/parity/canonical/verify__select_too_many_422.json b/tests/fixtures/parity/canonical/verify__select_too_many_422.json new file mode 100644 index 0000000..db644a3 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__select_too_many_422.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Cannot select more than 20 claims.", + "code": "too_many_items", + "errors": [ + { + "loc": [ + "body", + "claims" + ], + "msg": "Cannot select more than 20 claims.", + "type": "too_many_items" + } + ] + } +} diff --git a/tests/fixtures/parity/canonical/verify__select_unknown_task_404.json b/tests/fixtures/parity/canonical/verify__select_unknown_task_404.json new file mode 100644 index 0000000..ed0bc13 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__select_unknown_task_404.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Task not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json new file mode 100644 index 0000000..799ffa9 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": null, + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_completed.json b/tests/fixtures/parity/canonical/verify__status_completed.json new file mode 100644 index 0000000..c0ae291 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_completed.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "completed_at": "2026-10-01T09:28:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_completed_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while.json new file mode 100644 index 0000000..c0ae291 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "completed_at": "2026-10-01T09:28:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..64eb71f --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "completed_at": "2026-10-02T00:03:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..2cad36e --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "completed_at": "2026-10-01T20:00:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/verify__status_completed_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..64eb71f --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_completed_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "completed_at": "2026-10-02T00:03:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/verify__status_completed_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..2cad36e --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_completed_modified_at_same_day_hours_apart.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "completed_at": "2026-10-01T20:00:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_failed.json b/tests/fixtures/parity/canonical/verify__status_failed.json new file mode 100644 index 0000000..f0674e2 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_failed.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "research_empty", + "detail": "No sources about the claim were found.", + "hint": null, + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_failed_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_failed_after_a_while.json new file mode 100644 index 0000000..7deb947 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_failed_after_a_while.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "conclusion_failed", + "detail": "The check stopped while writing the conclusion.", + "hint": null, + "failure_class": "internal", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#internal" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_failed_after_a_while_framing.json b/tests/fixtures/parity/canonical/verify__status_failed_after_a_while_framing.json new file mode 100644 index 0000000..1329048 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_failed_after_a_while_framing.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "framing_failed", + "detail": "The input could not be read as a claim.", + "hint": null, + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_failed_retryable.json b/tests/fixtures/parity/canonical/verify__status_failed_retryable.json new file mode 100644 index 0000000..b7594ed --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_failed_retryable.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "adjudication_failed", + "detail": "The check stopped while the reviewers scored the evidence.", + "hint": null, + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_needs_input.json b/tests/fixtures/parity/canonical/verify__status_needs_input.json new file mode 100644 index 0000000..194a4c6 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_needs_input.json @@ -0,0 +1,24 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "reason": "multi_claim", + "claims": [ + { + "claim": "The Earth is round.", + "domain": "Science" + }, + { + "claim": "Water boils at 100C at sea level.", + "domain": "Science" + } + ], + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select." + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_not_a_claim.json b/tests/fixtures/parity/canonical/verify__status_not_a_claim.json new file mode 100644 index 0000000..dea790c --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_not_a_claim.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_not_a_claim_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_not_a_claim_after_a_while.json new file mode 100644 index 0000000..d8a3007 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_not_a_claim_after_a_while.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_not_yours_404.json b/tests/fixtures/parity/canonical/verify__status_not_yours_404.json new file mode 100644 index 0000000..ed0bc13 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_not_yours_404.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Task not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_completed.json b/tests/fixtures/parity/canonical/verify__status_older_run_completed.json new file mode 100644 index 0000000..c0ae291 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_older_run_completed.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "completed_at": "2026-10-01T09:28:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json b/tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json new file mode 100644 index 0000000..3966046 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "task_error", + "detail": "The check stopped on an error on our side.", + "hint": null, + "failure_class": "internal", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#internal" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json b/tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json new file mode 100644 index 0000000..f0674e2 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "research_empty", + "detail": "No sources about the claim were found.", + "hint": null, + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json b/tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json new file mode 100644 index 0000000..8ff9472 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "progress": { + "step": "research", + "index": 2, + "total": 5, + "elapsed_seconds": 2, + "poll_after_seconds": 5 + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_processing.json b/tests/fixtures/parity/canonical/verify__status_processing.json new file mode 100644 index 0000000..8ff9472 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_processing.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "progress": { + "step": "research", + "index": 2, + "total": 5, + "elapsed_seconds": 2, + "poll_after_seconds": 5 + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_processing_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_processing_after_a_while.json new file mode 100644 index 0000000..077dee8 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_processing_after_a_while.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "progress": { + "step": "adjudication", + "index": 4, + "total": 5, + "elapsed_seconds": 2, + "poll_after_seconds": 5 + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_task_stuck.json b/tests/fixtures/parity/canonical/verify__status_task_stuck.json new file mode 100644 index 0000000..8230e8c --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_task_stuck.json @@ -0,0 +1,20 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "task_stuck", + "detail": "The check stopped responding before it finished.", + "hint": null, + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_unknown_404.json b/tests/fixtures/parity/canonical/verify__status_unknown_404.json new file mode 100644 index 0000000..ed0bc13 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_unknown_404.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Task not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/verify__submit_202.json b/tests/fixtures/parity/canonical/verify__submit_202.json new file mode 100644 index 0000000..b510371 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__submit_202.json @@ -0,0 +1,12 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/verify__submit_202_options.json b/tests/fixtures/parity/canonical/verify__submit_202_options.json new file mode 100644 index 0000000..b510371 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__submit_202_options.json @@ -0,0 +1,12 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/verify__submit_202_text_alias.json b/tests/fixtures/parity/canonical/verify__submit_202_text_alias.json new file mode 100644 index 0000000..b510371 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__submit_202_text_alias.json @@ -0,0 +1,12 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/canonical/verify__unauthenticated_401.json b/tests/fixtures/parity/canonical/verify__unauthenticated_401.json new file mode 100644 index 0000000..1c3df51 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__unauthenticated_401.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_200.json b/tests/fixtures/parity/canonical/verify__verification_200.json new file mode 100644 index 0000000..e6d5f88 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_200.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_200_covered.json b/tests/fixtures/parity/canonical/verify__verification_200_covered.json new file mode 100644 index 0000000..ab1972e --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_200_covered.json @@ -0,0 +1,93 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "covered", + "reasons": [], + "certificate_id": "df2c8d27663c61f5a32c2c628d182f81", + "certificate_url": "https://lenz.io/certificate/df2c8d27663c61f5a32c2c628d182f81", + "as_of": "2026-03-10T12:01:30+00:00", + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..9b01ef7 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "completed_at": "2026-10-02T00:03:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_null.json b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_null.json new file mode 100644 index 0000000..3cd999b --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_null.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..6a57a82 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_same_day_hours_apart.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "completed_at": "2026-10-01T20:00:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_set.json b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_set.json new file mode 100644 index 0000000..9b01ef7 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_set.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "completed_at": "2026-10-02T00:03:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_200_result_json_conclusion_only.json b/tests/fixtures/parity/canonical/verify__verification_200_result_json_conclusion_only.json new file mode 100644 index 0000000..68f5a3f --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_200_result_json_conclusion_only.json @@ -0,0 +1,56 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [], + "presumed_intent": "", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "sources": [], + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_pro": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "debate_con": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "panel_agreement": "split" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_200_result_json_empty.json b/tests/fixtures/parity/canonical/verify__verification_200_result_json_empty.json new file mode 100644 index 0000000..68f5a3f --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_200_result_json_empty.json @@ -0,0 +1,56 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [], + "presumed_intent": "", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "sources": [], + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_pro": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "debate_con": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "panel_agreement": "split" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_200_result_json_old_shape_strings.json b/tests/fixtures/parity/canonical/verify__verification_200_result_json_old_shape_strings.json new file mode 100644 index 0000000..d236164 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_200_result_json_old_shape_strings.json @@ -0,0 +1,72 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [], + "presumed_intent": "", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "sources": [ + { + "source_name": "", + "title": "", + "url": "https://old.example/a", + "snippet": "Old row.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "", + "score": null, + "reasoning": "", + "warnings": [] + } + ], + "debate_pro": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "debate_con": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "panel_agreement": "split" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_404.json b/tests/fixtures/parity/canonical/verify__verification_404.json new file mode 100644 index 0000000..f1a8c33 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_404.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_by_task_id_200.json b/tests/fixtures/parity/canonical/verify__verification_by_task_id_200.json new file mode 100644 index 0000000..e6d5f88 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_by_task_id_200.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_failed_409.json b/tests/fixtures/parity/canonical/verify__verification_failed_409.json new file mode 100644 index 0000000..b7c3f82 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_failed_409.json @@ -0,0 +1,23 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This check failed and has no result.", + "code": "verification_failed", + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "hint": "Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "failure": { + "code": "research_empty", + "detail": "No sources about the claim were found.", + "hint": null, + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_failed_409_not_a_claim_after_a_while.json b/tests/fixtures/parity/canonical/verify__verification_failed_409_not_a_claim_after_a_while.json new file mode 100644 index 0000000..9479d57 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_failed_409_not_a_claim_after_a_while.json @@ -0,0 +1,23 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This check failed and has no result.", + "code": "verification_failed", + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure": { + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_not_ready_409.json b/tests/fixtures/parity/canonical/verify__verification_not_ready_409.json new file mode 100644 index 0000000..d5ace3f --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_not_ready_409.json @@ -0,0 +1,15 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This check is still running.", + "code": "verification_not_ready", + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "hint": "Poll GET /verify/status/7cff18da2d979d6370e54a160be121ad until it completes, then read its result." + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_not_yours_404.json b/tests/fixtures/parity/canonical/verify__verification_not_yours_404.json new file mode 100644 index 0000000..f1a8c33 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_not_yours_404.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_public_anonymous.json b/tests/fixtures/parity/canonical/verify__verification_public_anonymous.json new file mode 100644 index 0000000..aec1fd3 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_public_anonymous.json @@ -0,0 +1,83 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "public", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": null + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_public_of_another_user.json b/tests/fixtures/parity/canonical/verify__verification_public_of_another_user.json new file mode 100644 index 0000000..4ccbda8 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_public_of_another_user.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "public", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_unknown_task_404.json b/tests/fixtures/parity/canonical/verify__verification_unknown_task_404.json new file mode 100644 index 0000000..e577e2e --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_unknown_task_404.json @@ -0,0 +1,13 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Not found.", + "code": "not_found", + "hint": "Unknown id. If this is a task_id, poll GET /verify/status/dddddddddddddddddddddddddddddddd for its status." + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_zero_retention_expired_410.json b/tests/fixtures/parity/canonical/verify__verification_zero_retention_expired_410.json new file mode 100644 index 0000000..010b258 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_zero_retention_expired_410.json @@ -0,0 +1,13 @@ +{ + "status": 410, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This verification is no longer available: its account removes verifications after a set period.", + "code": "purged", + "purged_at": "2026-10-01T09:07:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_zero_retention_served.json b/tests/fixtures/parity/canonical/verify__verification_zero_retention_served.json new file mode 100644 index 0000000..fca733d --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_zero_retention_served.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical.", + "executive_summary": "The claim is well supported by evidence.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/canonical/webhook__certificate_timestamped.json b/tests/fixtures/parity/canonical/webhook__certificate_timestamped.json new file mode 100644 index 0000000..f2093db --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__certificate_timestamped.json @@ -0,0 +1,25 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "certificate.timestamped", + "verification_id": "68ca4aaf", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "batch_id": null, + "status": "completed", + "coverage": { + "status": "covered", + "reasons": [], + "certificate_id": "def7232d27eda03cf261569aee9948c5", + "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "as_of": "2026-09-03T10:00:00+00:00", + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "event_id": "evt_0b92b443eb69b9c71434fdc6" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__certificate_timestamped_review_child.json b/tests/fixtures/parity/canonical/webhook__certificate_timestamped_review_child.json new file mode 100644 index 0000000..80582ef --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__certificate_timestamped_review_child.json @@ -0,0 +1,26 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "certificate.timestamped", + "verification_id": "68ca4aaf", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "batch_id": null, + "status": "completed", + "coverage": { + "status": "covered", + "reasons": [], + "certificate_id": "def7232d27eda03cf261569aee9948c5", + "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "as_of": "2026-09-03T10:00:00+00:00", + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + }, + "review_id": "d6b2bd72", + "attempt": 1, + "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "event_id": "evt_0b92b443eb69b9c71434fdc6" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__citecheck_completed.json b/tests/fixtures/parity/canonical/webhook__citecheck_completed.json new file mode 100644 index 0000000..ce60399 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__citecheck_completed.json @@ -0,0 +1,133 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "citecheck.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "citecheck_id": "98c0e7d5", + "status": "completed", + "citecheck": { + "citecheck_id": "98c0e7d5", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 1 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "contradicted", + "source": "support", + "is_issue": true + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "contradicted", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [ + { + "citation_index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "finding": "contradicted", + "source": "support", + "snippet": "The source says so.", + "rationale": "The page states it.", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "failure": null + } + ], + "citation_failures": [], + "failure": null, + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:42:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__citecheck_failed.json b/tests/fixtures/parity/canonical/webhook__citecheck_failed.json new file mode 100644 index 0000000..7474d1c --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__citecheck_failed.json @@ -0,0 +1,46 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "citecheck.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "citecheck_id": "98c0e7d5", + "status": "failed", + "citecheck": { + "citecheck_id": "98c0e7d5", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 20 + }, + "summary": { + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [], + "citation_issues": [], + "citation_failures": [], + "failure": { + "failure_reason": "no_citations", + "failure_class": "invalid_input", + "retryable": false, + "hint": "The text has no citation to check: no link, DOI or numbered reference with one.", + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_citations": null + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:42:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__review_completed.json b/tests/fixtures/parity/canonical/webhook__review_completed.json new file mode 100644 index 0000000..78cf7d2 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__review_completed.json @@ -0,0 +1,155 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "review.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "8cb2f001", + "status": "completed", + "review": { + "review_id": "8cb2f001", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 1, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 11 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "8ceed6f0", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", + "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "5ccac77f53ff49328ec134fb29d66542", + "verification_id": "8ceed6f0", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:56:00.123456+00:00", + "modified_at": null, + "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", + "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "failure": null + }, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T10:03:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__review_completed_key_default_url.json b/tests/fixtures/parity/canonical/webhook__review_completed_key_default_url.json new file mode 100644 index 0000000..0631d53 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__review_completed_key_default_url.json @@ -0,0 +1,104 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "review.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "8cb2f001", + "status": "completed", + "review": { + "review_id": "8cb2f001", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:42:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__review_completed_oversized_rebuilt.json b/tests/fixtures/parity/canonical/webhook__review_completed_oversized_rebuilt.json new file mode 100644 index 0000000..e96d947 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__review_completed_oversized_rebuilt.json @@ -0,0 +1,10 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "review.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "8cb2f001", + "status": "completed" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__review_failed.json b/tests/fixtures/parity/canonical/webhook__review_failed.json new file mode 100644 index 0000000..f382954 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__review_failed.json @@ -0,0 +1,72 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "review.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "8cb2f001", + "status": "failed", + "review": { + "review_id": "8cb2f001", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": null, + "claim_limit": 20, + "claim_limit_reached": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "failure_reason": "no_claim", + "failure_class": "invalid_input", + "retryable": false, + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:42:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_completed.json b/tests/fixtures/parity/canonical/webhook__verification_completed.json new file mode 100644 index 0000000..64c7f2c --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_completed.json @@ -0,0 +1,109 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.completed", + "event_id": "evt_0b92b443eb69b9c71434fdc6", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "verification_id": "68ca4aaf", + "batch_id": null, + "status": "completed", + "verification": { + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "visibility": "private", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "presumed_intent": "Check where a landmark is.", + "verdict": "False", + "confidence": "high", + "lenz_score": 1, + "key_finding": "The Eiffel Tower is in Paris.", + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "warnings": [ + "Only two sources were found." + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "created_at": "2026-10-01T09:21:00.123456+00:00", + "sources": [ + { + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "date": "2025-01-15" + }, + { + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Evidence", + "score": 1.0, + "reasoning": "Both sources place the tower in Paris.", + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Evidence", + "score": 2.0, + "reasoning": "The encyclopedia entry is unambiguous.", + "warnings": [] + } + ], + "debate_pro": { + "role": "pro", + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable." + }, + "debate_con": { + "role": "con", + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + }, + "completed_at": "2026-10-01T09:21:00.123456+00:00" + } + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:28:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_completed_in_batch.json b/tests/fixtures/parity/canonical/webhook__verification_completed_in_batch.json new file mode 100644 index 0000000..f5a306c --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_completed_in_batch.json @@ -0,0 +1,109 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.completed", + "event_id": "evt_0b92b443eb69b9c71434fdc6", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "verification_id": "68ca4aaf", + "batch_id": "4ee0c3aa1b671eaa", + "status": "completed", + "verification": { + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "visibility": "private", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "presumed_intent": "Check where a landmark is.", + "verdict": "False", + "confidence": "high", + "lenz_score": 1, + "key_finding": "The Eiffel Tower is in Paris.", + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "warnings": [ + "Only two sources were found." + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "created_at": "2026-10-01T09:28:00.123456+00:00", + "sources": [ + { + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "date": "2025-01-15" + }, + { + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Evidence", + "score": 1.0, + "reasoning": "Both sources place the tower in Paris.", + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Evidence", + "score": 2.0, + "reasoning": "The encyclopedia entry is unambiguous.", + "warnings": [] + } + ], + "debate_pro": { + "role": "pro", + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable." + }, + "debate_con": { + "role": "con", + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + }, + "completed_at": "2026-10-01T09:28:00.123456+00:00" + } + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:35:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..2bbe988 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,109 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.completed", + "event_id": "evt_0b92b443eb69b9c71434fdc6", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "verification_id": "68ca4aaf", + "batch_id": null, + "status": "completed", + "verification": { + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "visibility": "private", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "presumed_intent": "Check where a landmark is.", + "verdict": "False", + "confidence": "high", + "lenz_score": 1, + "key_finding": "The Eiffel Tower is in Paris.", + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "warnings": [ + "Only two sources were found." + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "created_at": "2026-10-01T23:55:00.123456+00:00", + "sources": [ + { + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "date": "2025-01-15" + }, + { + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Evidence", + "score": 1.0, + "reasoning": "Both sources place the tower in Paris.", + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Evidence", + "score": 2.0, + "reasoning": "The encyclopedia entry is unambiguous.", + "warnings": [] + } + ], + "debate_pro": { + "role": "pro", + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable." + }, + "debate_con": { + "role": "con", + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + }, + "completed_at": "2026-10-02T00:03:00.123456+00:00" + } + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:35:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..fa61911 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_same_day_hours_apart.json @@ -0,0 +1,109 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.completed", + "event_id": "evt_0b92b443eb69b9c71434fdc6", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "verification_id": "68ca4aaf", + "batch_id": null, + "status": "completed", + "verification": { + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "visibility": "private", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "presumed_intent": "Check where a landmark is.", + "verdict": "False", + "confidence": "high", + "lenz_score": 1, + "key_finding": "The Eiffel Tower is in Paris.", + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "warnings": [ + "Only two sources were found." + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "created_at": "2026-10-01T08:00:00.123456+00:00", + "sources": [ + { + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "date": "2025-01-15" + }, + { + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Evidence", + "score": 1.0, + "reasoning": "Both sources place the tower in Paris.", + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Evidence", + "score": 2.0, + "reasoning": "The encyclopedia entry is unambiguous.", + "warnings": [] + } + ], + "debate_pro": { + "role": "pro", + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable." + }, + "debate_con": { + "role": "con", + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + }, + "completed_at": "2026-10-01T20:00:00.123456+00:00" + } + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:28:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_failed_insufficient_evidence.json b/tests/fixtures/parity/canonical/webhook__verification_failed_insufficient_evidence.json new file mode 100644 index 0000000..f47e7ee --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_failed_insufficient_evidence.json @@ -0,0 +1,26 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.failed", + "event_id": "evt_7cff18da2d979d6370e54a16", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": null, + "batch_id": null, + "status": "failed", + "verification": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "research_insufficient", + "detail": "A sentence on what happened.", + "hint": null, + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors" + } + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:14:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_failed_not_a_claim.json b/tests/fixtures/parity/canonical/webhook__verification_failed_not_a_claim.json new file mode 100644 index 0000000..cf284d5 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_failed_not_a_claim.json @@ -0,0 +1,26 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.failed", + "event_id": "evt_7cff18da2d979d6370e54a16", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": null, + "batch_id": null, + "status": "failed", + "verification": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "no_checkable_claim", + "detail": "A sentence on what happened.", + "hint": null, + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors" + } + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:14:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_failed_upstream_unavailable.json b/tests/fixtures/parity/canonical/webhook__verification_failed_upstream_unavailable.json new file mode 100644 index 0000000..322b4ba --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_failed_upstream_unavailable.json @@ -0,0 +1,26 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.failed", + "event_id": "evt_7cff18da2d979d6370e54a16", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": null, + "batch_id": null, + "status": "failed", + "verification": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "framing_failed", + "detail": "A sentence on what happened.", + "hint": null, + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors" + } + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:14:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_needs_input_multi_claim.json b/tests/fixtures/parity/canonical/webhook__verification_needs_input_multi_claim.json new file mode 100644 index 0000000..0961dbb --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_needs_input_multi_claim.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.needs_input", + "event_id": "evt_7cff18da2d979d6370e54a16", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": null, + "batch_id": null, + "status": "needs_input", + "verification": { + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "reason": "multi_claim", + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "claims": [ + { + "claim": "The Earth is round.", + "domain": "" + }, + { + "claim": "The Moon is made of cheese.", + "domain": "Science" + } + ] + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:14:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/expected/account__library_empty.json b/tests/fixtures/parity/expected/account__library_empty.json new file mode 100644 index 0000000..d628806 --- /dev/null +++ b/tests/fixtures/parity/expected/account__library_empty.json @@ -0,0 +1,9 @@ +{ + "dump": { + "items": [], + "page": 1, + "page_size": 20, + "total": 0 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/account__library_item_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/account__library_item_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..dedd58d --- /dev/null +++ b/tests/fixtures/parity/expected/account__library_item_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,25 @@ +{ + "dump": { + "items": [ + { + "claim": "A library claim.", + "confidence": "low", + "created_at": "2026-10-01T23:55:00.123456+00:00", + "domain": "", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 1, + "page_size": 20, + "total": 1 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/account__library_item_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/account__library_item_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..7edcd92 --- /dev/null +++ b/tests/fixtures/parity/expected/account__library_item_modified_at_same_day_hours_apart.json @@ -0,0 +1,25 @@ +{ + "dump": { + "items": [ + { + "claim": "A library claim.", + "confidence": "low", + "created_at": "2026-10-01T08:00:00.123456+00:00", + "domain": "", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 1, + "page_size": 20, + "total": 1 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/account__library_items.json b/tests/fixtures/parity/expected/account__library_items.json new file mode 100644 index 0000000..72a7d37 --- /dev/null +++ b/tests/fixtures/parity/expected/account__library_items.json @@ -0,0 +1,55 @@ +{ + "dump": { + "items": [ + { + "claim": "Library claim 0.", + "confidence": "low", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + }, + { + "claim": "Library claim 1.", + "confidence": "low", + "created_at": "2026-10-01T09:28:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "7c83247c" + }, + { + "claim": "Library claim 2.", + "confidence": "low", + "created_at": "2026-10-01T09:42:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "b79019e4" + } + ], + "page": 1, + "page_size": 20, + "total": 3 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/account__library_no_match.json b/tests/fixtures/parity/expected/account__library_no_match.json new file mode 100644 index 0000000..d628806 --- /dev/null +++ b/tests/fixtures/parity/expected/account__library_no_match.json @@ -0,0 +1,9 @@ +{ + "dump": { + "items": [], + "page": 1, + "page_size": 20, + "total": 0 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/account__library_page_past_end.json b/tests/fixtures/parity/expected/account__library_page_past_end.json new file mode 100644 index 0000000..437be04 --- /dev/null +++ b/tests/fixtures/parity/expected/account__library_page_past_end.json @@ -0,0 +1,9 @@ +{ + "dump": { + "items": [], + "page": 2, + "page_size": 20, + "total": 1 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/account__library_paginated_second_page.json b/tests/fixtures/parity/expected/account__library_paginated_second_page.json new file mode 100644 index 0000000..6d4d3d3 --- /dev/null +++ b/tests/fixtures/parity/expected/account__library_paginated_second_page.json @@ -0,0 +1,40 @@ +{ + "dump": { + "items": [ + { + "claim": "Paged claim 020.", + "confidence": "low", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "domain": "", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + }, + { + "claim": "Paged claim 021.", + "confidence": "low", + "created_at": "2026-10-01T09:28:00.123456+00:00", + "domain": "", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "7c83247c" + } + ], + "page": 2, + "page_size": 20, + "total": 22 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/account__me_usage_extra_credits.json b/tests/fixtures/parity/expected/account__me_usage_extra_credits.json new file mode 100644 index 0000000..4b02079 --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_extra_credits.json @@ -0,0 +1,62 @@ +{ + "dump": { + "ask": { + "bonus": 250, + "credits": 250, + "quota_remaining": 350, + "quota_total": 350, + "quota_used": 0, + "remaining": 350 + }, + "assess": { + "bonus": 250, + "credits": 250, + "quota_remaining": 350, + "quota_total": 350, + "quota_used": 0, + "remaining": 350 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 250, + "extra": 250, + "remaining": 350, + "resets_at": "2026-10-01T09:07:00+00:00", + "total": 350, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "verify": { + "bonus": 25, + "credits": 25, + "quota_remaining": 35, + "quota_total": 35, + "quota_used": 0, + "remaining": 35 + } + }, + "render": "Lenz usage (Free plan)\n 350 credits left (≈ 35 verifications · 350 assessments)\n Verify: 35 left (0 / 35 quota + 25 extra · 10 credits each · 5 at depth \"low\")\n Ask: 350 left (0 / 350 quota + 250 extra · 1 credit each)\n Assess: 350 left (0 / 350 quota + 250 extra · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 350,\n \"used\": 0,\n \"remaining\": 350,\n \"extra\": 250,\n \"bonus\": 250,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 35,\n \"quota_remaining\": 35,\n \"bonus\": 25,\n \"credits\": 25,\n \"remaining\": 35\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 350,\n \"quota_remaining\": 350,\n \"bonus\": 250,\n \"credits\": 250,\n \"remaining\": 350\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 350,\n \"quota_remaining\": 350,\n \"bonus\": 250,\n \"credits\": 250,\n \"remaining\": 350\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" +} diff --git a/tests/fixtures/parity/expected/account__me_usage_extra_only.json b/tests/fixtures/parity/expected/account__me_usage_extra_only.json new file mode 100644 index 0000000..fbd9a1f --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_extra_only.json @@ -0,0 +1,62 @@ +{ + "dump": { + "ask": { + "bonus": 37, + "credits": 37, + "quota_remaining": 37, + "quota_total": 137, + "quota_used": 100, + "remaining": 37 + }, + "assess": { + "bonus": 37, + "credits": 37, + "quota_remaining": 37, + "quota_total": 137, + "quota_used": 100, + "remaining": 37 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 37, + "extra": 37, + "remaining": 37, + "resets_at": "2026-10-01T09:07:00+00:00", + "total": 137, + "used": 100 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "verify": { + "bonus": 3, + "credits": 3, + "quota_remaining": 3, + "quota_total": 13, + "quota_used": 10, + "remaining": 3 + } + }, + "render": "Lenz usage (Free plan)\n 37 credits left (≈ 3 verifications · 37 assessments)\n Verify: 3 left (10 / 13 quota + 3 extra · 10 credits each · 5 at depth \"low\")\n Ask: 37 left (100 / 137 quota + 37 extra · 1 credit each)\n Assess: 37 left (100 / 137 quota + 37 extra · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 137,\n \"used\": 100,\n \"remaining\": 37,\n \"extra\": 37,\n \"bonus\": 37,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 10,\n \"quota_total\": 13,\n \"quota_remaining\": 3,\n \"bonus\": 3,\n \"credits\": 3,\n \"remaining\": 3\n },\n \"ask\": {\n \"quota_used\": 100,\n \"quota_total\": 137,\n \"quota_remaining\": 37,\n \"bonus\": 37,\n \"credits\": 37,\n \"remaining\": 37\n },\n \"assess\": {\n \"quota_used\": 100,\n \"quota_total\": 137,\n \"quota_remaining\": 37,\n \"bonus\": 37,\n \"credits\": 37,\n \"remaining\": 37\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" +} diff --git a/tests/fixtures/parity/expected/account__me_usage_extract_calls.json b/tests/fixtures/parity/expected/account__me_usage_extract_calls.json new file mode 100644 index 0000000..cfb8de5 --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_extract_calls.json @@ -0,0 +1,62 @@ +{ + "dump": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 100, + "resets_at": "2026-10-01T09:07:00+00:00", + "total": 100, + "used": 0 + }, + "extract": { + "calls_today": 7, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 10, + "quota_total": 10, + "quota_used": 0, + "remaining": 10 + } + }, + "render": "Lenz usage (Free plan)\n 100 credits left (≈ 10 verifications · 100 assessments)\n Verify: 10 left (0 / 10 quota · 10 credits each · 5 at depth \"low\")\n Ask: 100 left (0 / 100 quota · 1 credit each)\n Assess: 100 left (0 / 100 quota · 1 credit each)\n Extract: 7 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 7,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" +} diff --git a/tests/fixtures/parity/expected/account__me_usage_free.json b/tests/fixtures/parity/expected/account__me_usage_free.json new file mode 100644 index 0000000..5d7b08d --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_free.json @@ -0,0 +1,62 @@ +{ + "dump": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 100, + "resets_at": "2026-10-01T09:07:00+00:00", + "total": 100, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 10, + "quota_total": 10, + "quota_used": 0, + "remaining": 10 + } + }, + "render": "Lenz usage (Free plan)\n 100 credits left (≈ 10 verifications · 100 assessments)\n Verify: 10 left (0 / 10 quota · 10 credits each · 5 at depth \"low\")\n Ask: 100 left (0 / 100 quota · 1 credit each)\n Assess: 100 left (0 / 100 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" +} diff --git a/tests/fixtures/parity/expected/account__me_usage_free_partly_spent.json b/tests/fixtures/parity/expected/account__me_usage_free_partly_spent.json new file mode 100644 index 0000000..61b0e4c --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_free_partly_spent.json @@ -0,0 +1,62 @@ +{ + "dump": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 83, + "quota_total": 100, + "quota_used": 17, + "remaining": 83 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 83, + "quota_total": 100, + "quota_used": 17, + "remaining": 83 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 83, + "resets_at": "2026-10-01T09:07:00+00:00", + "total": 100, + "used": 17 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 8, + "quota_total": 10, + "quota_used": 2, + "remaining": 8 + } + }, + "render": "Lenz usage (Free plan)\n 83 credits left (≈ 8 verifications · 83 assessments)\n Verify: 8 left (2 / 10 quota · 10 credits each · 5 at depth \"low\")\n Ask: 83 left (17 / 100 quota · 1 credit each)\n Assess: 83 left (17 / 100 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 17,\n \"remaining\": 83,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 2,\n \"quota_total\": 10,\n \"quota_remaining\": 8,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 8\n },\n \"ask\": {\n \"quota_used\": 17,\n \"quota_total\": 100,\n \"quota_remaining\": 83,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 83\n },\n \"assess\": {\n \"quota_used\": 17,\n \"quota_total\": 100,\n \"quota_remaining\": 83,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 83\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" +} diff --git a/tests/fixtures/parity/expected/account__me_usage_plus.json b/tests/fixtures/parity/expected/account__me_usage_plus.json new file mode 100644 index 0000000..240dcb2 --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_plus.json @@ -0,0 +1,62 @@ +{ + "dump": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 500, + "quota_total": 500, + "quota_used": 0, + "remaining": 500 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 500, + "quota_total": 500, + "quota_used": 0, + "remaining": 500 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 500, + "resets_at": "2026-10-01T09:07:00.123456+00:00", + "total": 500, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "plus", + "plan_label": "Plus", + "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 50, + "quota_total": 50, + "quota_used": 0, + "remaining": 50 + } + }, + "render": "Lenz usage (Plus plan)\n 500 credits left (≈ 50 verifications · 500 assessments)\n Verify: 50 left (0 / 50 quota · 10 credits each · 5 at depth \"low\")\n Ask: 500 left (0 / 500 quota · 1 credit each)\n Assess: 500 left (0 / 500 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"plus\",\n \"plan_label\": \"Plus\",\n \"quota_resets_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"credits\": {\n \"total\": 500,\n \"used\": 0,\n \"remaining\": 500,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00.123456+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 50,\n \"quota_remaining\": 50,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 50\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 500,\n \"quota_remaining\": 500,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 500\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 500,\n \"quota_remaining\": 500,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 500\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" +} diff --git a/tests/fixtures/parity/expected/account__me_usage_pro.json b/tests/fixtures/parity/expected/account__me_usage_pro.json new file mode 100644 index 0000000..6bd10f7 --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_pro.json @@ -0,0 +1,62 @@ +{ + "dump": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 5000, + "quota_total": 5000, + "quota_used": 0, + "remaining": 5000 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 5000, + "quota_total": 5000, + "quota_used": 0, + "remaining": 5000 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 5000, + "resets_at": "2026-10-01T09:07:00.123456+00:00", + "total": 5000, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "pro", + "plan_label": "Pro", + "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 500, + "quota_total": 500, + "quota_used": 0, + "remaining": 500 + } + }, + "render": "Lenz usage (Pro plan)\n 5000 credits left (≈ 500 verifications · 5000 assessments)\n Verify: 500 left (0 / 500 quota · 10 credits each · 5 at depth \"low\")\n Ask: 5000 left (0 / 5000 quota · 1 credit each)\n Assess: 5000 left (0 / 5000 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"pro\",\n \"plan_label\": \"Pro\",\n \"quota_resets_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"credits\": {\n \"total\": 5000,\n \"used\": 0,\n \"remaining\": 5000,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00.123456+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 500,\n \"quota_remaining\": 500,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 500\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 5000,\n \"quota_remaining\": 5000,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 5000\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 5000,\n \"quota_remaining\": 5000,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 5000\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" +} diff --git a/tests/fixtures/parity/expected/account__me_usage_pro_extra.json b/tests/fixtures/parity/expected/account__me_usage_pro_extra.json new file mode 100644 index 0000000..ca22ae7 --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_pro_extra.json @@ -0,0 +1,62 @@ +{ + "dump": { + "ask": { + "bonus": 1000, + "credits": 1000, + "quota_remaining": 6000, + "quota_total": 6000, + "quota_used": 0, + "remaining": 6000 + }, + "assess": { + "bonus": 1000, + "credits": 1000, + "quota_remaining": 6000, + "quota_total": 6000, + "quota_used": 0, + "remaining": 6000 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 1000, + "extra": 1000, + "remaining": 6000, + "resets_at": "2026-10-01T09:07:00.123456+00:00", + "total": 6000, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "pro", + "plan_label": "Pro", + "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "verify": { + "bonus": 100, + "credits": 100, + "quota_remaining": 600, + "quota_total": 600, + "quota_used": 0, + "remaining": 600 + } + }, + "render": "Lenz usage (Pro plan)\n 6000 credits left (≈ 600 verifications · 6000 assessments)\n Verify: 600 left (0 / 600 quota + 100 extra · 10 credits each · 5 at depth \"low\")\n Ask: 6000 left (0 / 6000 quota + 1000 extra · 1 credit each)\n Assess: 6000 left (0 / 6000 quota + 1000 extra · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"pro\",\n \"plan_label\": \"Pro\",\n \"quota_resets_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"credits\": {\n \"total\": 6000,\n \"used\": 0,\n \"remaining\": 6000,\n \"extra\": 1000,\n \"bonus\": 1000,\n \"resets_at\": \"2026-10-01T09:07:00.123456+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 600,\n \"quota_remaining\": 600,\n \"bonus\": 100,\n \"credits\": 100,\n \"remaining\": 600\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 6000,\n \"quota_remaining\": 6000,\n \"bonus\": 1000,\n \"credits\": 1000,\n \"remaining\": 6000\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 6000,\n \"quota_remaining\": 6000,\n \"bonus\": 1000,\n \"credits\": 1000,\n \"remaining\": 6000\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" +} diff --git a/tests/fixtures/parity/expected/account__me_usage_stale_quota_row.json b/tests/fixtures/parity/expected/account__me_usage_stale_quota_row.json new file mode 100644 index 0000000..5d7b08d --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_stale_quota_row.json @@ -0,0 +1,62 @@ +{ + "dump": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 100, + "resets_at": "2026-10-01T09:07:00+00:00", + "total": 100, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 10, + "quota_total": 10, + "quota_used": 0, + "remaining": 10 + } + }, + "render": "Lenz usage (Free plan)\n 100 credits left (≈ 10 verifications · 100 assessments)\n Verify: 10 left (0 / 10 quota · 10 credits each · 5 at depth \"low\")\n Ask: 100 left (0 / 100 quota · 1 credit each)\n Assess: 100 left (0 / 100 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__401_no_key.json b/tests/fixtures/parity/expected/assess__401_no_key.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/assess__401_no_key.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/assess__402_no_credits.json b/tests/fixtures/parity/expected/assess__402_no_credits.json new file mode 100644 index 0000000..732bc39 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__402_no_credits.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "No remaining /assess units.", + "code": "no_credits", + "cost": 1, + "credit_balance": 0, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "No remaining /assess units.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "No remaining /assess units.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "No remaining /assess units.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 0, + "request_id": "", + "requested": null, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/assess__422_blank_item.json b/tests/fixtures/parity/expected/assess__422_blank_item.json new file mode 100644 index 0000000..4b35c85 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__422_blank_item.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "claims[1] is blank.", + "code": "blank_item", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "claims[1] is blank.\n Fix: Check the request body against the OpenAPI spec.", + "message": "claims[1] is blank.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "claims[1] is blank.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/assess__422_blank_text.json b/tests/fixtures/parity/expected/assess__422_blank_text.json new file mode 100644 index 0000000..872ff11 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__422_blank_text.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Text is required.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Text is required.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Text is required.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Text is required.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/assess__422_input_conflict.json b/tests/fixtures/parity/expected/assess__422_input_conflict.json new file mode 100644 index 0000000..215d4af --- /dev/null +++ b/tests/fixtures/parity/expected/assess__422_input_conflict.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Send either `claim` (one text) or `claims` (a list of claims), not both.", + "code": "input_conflict", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Send either `claim` (one text) or `claims` (a list of claims), not both.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Send either `claim` (one text) or `claims` (a list of claims), not both.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Send either `claim` (one text) or `claims` (a list of claims), not both.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/assess__422_item_too_long.json b/tests/fixtures/parity/expected/assess__422_item_too_long.json new file mode 100644 index 0000000..1e678c0 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__422_item_too_long.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.", + "code": "item_too_long", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.\n Fix: Check the request body against the OpenAPI spec.", + "message": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/assess__422_no_input_field.json b/tests/fixtures/parity/expected/assess__422_no_input_field.json new file mode 100644 index 0000000..6760d1a --- /dev/null +++ b/tests/fixtures/parity/expected/assess__422_no_input_field.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/assess__422_too_many_items.json b/tests/fixtures/parity/expected/assess__422_too_many_items.json new file mode 100644 index 0000000..14bdc66 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__422_too_many_items.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "At most 20 claims per call; got 21. Split the list.", + "code": "too_many_items", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "At most 20 claims per call; got 21. Split the list.\n Fix: Check the request body against the OpenAPI spec.", + "message": "At most 20 claims per call; got 21. Split the list.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "At most 20 claims per call; got 21. Split the list.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/assess__422_wrong_type_claim.json b/tests/fixtures/parity/expected/assess__422_wrong_type_claim.json new file mode 100644 index 0000000..45b3d77 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__422_wrong_type_claim.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'string_type', 'loc': ['body', 'payload', 'claim'], 'msg': 'Input should be a valid string'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "claim" + ], + "msg": "Input should be a valid string", + "type": "string_type" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'string_type', 'loc': ['body', 'payload', 'claim'], 'msg': 'Input should be a valid string'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'string_type', 'loc': ['body', 'payload', 'claim'], 'msg': 'Input should be a valid string'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'string_type', 'loc': ['body', 'payload', 'claim'], 'msg': 'Input should be a valid string'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/assess__422_wrong_type_claims.json b/tests/fixtures/parity/expected/assess__422_wrong_type_claims.json new file mode 100644 index 0000000..7713b4c --- /dev/null +++ b/tests/fixtures/parity/expected/assess__422_wrong_type_claims.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'list_type', 'loc': ['body', 'payload', 'claims'], 'msg': 'Input should be a valid list'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "claims" + ], + "msg": "Input should be a valid list", + "type": "list_type" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'list_type', 'loc': ['body', 'payload', 'claims'], 'msg': 'Input should be a valid list'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'list_type', 'loc': ['body', 'payload', 'claims'], 'msg': 'Input should be a valid list'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'list_type', 'loc': ['body', 'payload', 'claims'], 'msg': 'Input should be a valid list'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/assess__502_framing_failed.json b/tests/fixtures/parity/expected/assess__502_framing_failed.json new file mode 100644 index 0000000..c8d0b2d --- /dev/null +++ b/tests/fixtures/parity/expected/assess__502_framing_failed.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Framing failed", + "code": "framing_failed", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Framing failed\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Framing failed", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Framing failed", + "status": 502 + } + }, + "request_id": "", + "retry_after": null, + "status_code": 502, + "type": "LenzAPIError" + } +} diff --git a/tests/fixtures/parity/expected/assess__503_capacity.json b/tests/fixtures/parity/expected/assess__503_capacity.json new file mode 100644 index 0000000..017e2bc --- /dev/null +++ b/tests/fixtures/parity/expected/assess__503_capacity.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 100, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/assess__503_list_all_upstream_unavailable.json b/tests/fixtures/parity/expected/assess__503_list_all_upstream_unavailable.json new file mode 100644 index 0000000..b61a2fb --- /dev/null +++ b/tests/fixtures/parity/expected/assess__503_list_all_upstream_unavailable.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Our model providers are temporarily unavailable. Please retry shortly. Nothing was charged.", + "code": "upstream_unavailable", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Our model providers are temporarily unavailable. Please retry shortly. Nothing was charged.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Our model providers are temporarily unavailable. Please retry shortly. Nothing was charged.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Our model providers are temporarily unavailable. Please retry shortly. Nothing was charged.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 90, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/assess__503_upstream_unavailable.json b/tests/fixtures/parity/expected/assess__503_upstream_unavailable.json new file mode 100644 index 0000000..7553c4c --- /dev/null +++ b/tests/fixtures/parity/expected/assess__503_upstream_unavailable.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Our model providers are temporarily unavailable. Please retry shortly.", + "code": "upstream_unavailable", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Our model providers are temporarily unavailable. Please retry shortly.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Our model providers are temporarily unavailable. Please retry shortly.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Our model providers are temporarily unavailable. Please retry shortly.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 90, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/assess__deep_tier_hit.json b/tests/fixtures/parity/expected/assess__deep_tier_hit.json new file mode 100644 index 0000000..89424ff --- /dev/null +++ b/tests/fixtures/parity/expected/assess__deep_tier_hit.json @@ -0,0 +1,26 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "Water boils at 100C.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "The claim is verified.", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": "https://lenz.io/api/v1/verifications/f3436f50" + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "True (high) — Water boils at 100C.\n The claim is verified.\n ask follow-ups: lenz ask f3436f50 \"\"\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Water boils at 100C.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": \"https://lenz.io/api/v1/verifications/f3436f50\",\n \"rationale\": \"The claim is verified.\",\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__idempotency_body_mismatch.json b/tests/fixtures/parity/expected/assess__idempotency_body_mismatch.json new file mode 100644 index 0000000..989af48 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__idempotency_body_mismatch.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Idempotency-Key reused with a different request body.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Idempotency-Key reused with a different request body.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/assess__idempotency_first.json b/tests/fixtures/parity/expected/assess__idempotency_first.json new file mode 100644 index 0000000..cb3d6d5 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__idempotency_first.json @@ -0,0 +1,26 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "The Earth orbits the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "True (high) — The Earth orbits the Sun.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Earth orbits the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__idempotency_replay.json b/tests/fixtures/parity/expected/assess__idempotency_replay.json new file mode 100644 index 0000000..cb3d6d5 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__idempotency_replay.json @@ -0,0 +1,26 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "The Earth orbits the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "True (high) — The Earth orbits the Sun.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Earth orbits the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__list_all_error_rows.json b/tests/fixtures/parity/expected/assess__list_all_error_rows.json new file mode 100644 index 0000000..edf8955 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__list_all_error_rows.json @@ -0,0 +1,40 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "hi", + "confidence": "low", + "dissent": null, + "error_code": "no_claim", + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "hello", + "confidence": "low", + "dissent": null, + "error_code": "no_claim", + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "Error (no_claim) — hi\n The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\nError (no_claim) — hello\n The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"hi\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\"\n },\n {\n \"claim\": \"hello\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\"\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__list_compound_item.json b/tests/fixtures/parity/expected/assess__list_compound_item.json new file mode 100644 index 0000000..6437e79 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__list_compound_item.json @@ -0,0 +1,43 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "Primary claim.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": "Assessed the main claim only. Send identified_claims as their own items to check the rest.", + "identified_claims": [ + "Second claim.", + "Third claim." + ], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Mostly False", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "A plain claim.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Mostly False", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "Mostly False (high) — Primary claim.\n also found:\n • Second claim.\n • Third claim.\n Assessed the main claim only. Send identified_claims as their own items to check the rest.\nMostly False (high) — A plain claim.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Primary claim.\",\n \"language\": \"en\",\n \"verdict\": \"Mostly False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [\n \"Second claim.\",\n \"Third claim.\"\n ],\n \"hint\": \"Assessed the main claim only. Send identified_claims as their own items to check the rest.\"\n },\n {\n \"claim\": \"A plain claim.\",\n \"language\": \"en\",\n \"verdict\": \"Mostly False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__list_mixed_rows.json b/tests/fixtures/parity/expected/assess__list_mixed_rows.json new file mode 100644 index 0000000..f92df93 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__list_mixed_rows.json @@ -0,0 +1,68 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "Water boils at 100 C at sea level.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "hello there", + "confidence": "low", + "dissent": null, + "error_code": "no_claim", + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "vendor is down", + "confidence": "low", + "dissent": null, + "error_code": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "cannot frame", + "confidence": "low", + "dissent": null, + "error_code": "framing_failed", + "hint": "This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; nothing was charged.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "True (high) — Water boils at 100 C at sea level.\nError (no_claim) — hello there\n The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\nError (upstream_unavailable) — vendor is down\n A model provider was unavailable for this item. Retry it; nothing was charged.\nError (framing_failed) — cannot frame\n This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; \nnothing was charged.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Water boils at 100 C at sea level.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"hello there\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\"\n },\n {\n \"claim\": \"vendor is down\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"upstream_unavailable\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"A model provider was unavailable for this item. Retry it; nothing was charged.\"\n },\n {\n \"claim\": \"cannot frame\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"framing_failed\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; nothing was charged.\"\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__memo_hit.json b/tests/fixtures/parity/expected/assess__memo_hit.json new file mode 100644 index 0000000..df5db6d --- /dev/null +++ b/tests/fixtures/parity/expected/assess__memo_hit.json @@ -0,0 +1,26 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "The Danube flows through Vienna.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "The river runs through the city.", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "True (high) — The Danube flows through Vienna.\n The river runs through the city.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Danube flows through Vienna.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"The river runs through the city.\",\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__replay_later_200.json b/tests/fixtures/parity/expected/assess__replay_later_200.json new file mode 100644 index 0000000..cb3d6d5 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__replay_later_200.json @@ -0,0 +1,26 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "The Earth orbits the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "True (high) — The Earth orbits the Sun.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Earth orbits the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__single_no_claim.json b/tests/fixtures/parity/expected/assess__single_no_claim.json new file mode 100644 index 0000000..2c5225e --- /dev/null +++ b/tests/fixtures/parity/expected/assess__single_no_claim.json @@ -0,0 +1,11 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [], + "error": "No verifiable claim detected", + "error_code": "no_claim", + "more_claims": [] + }, + "render": "No claim found.\n", + "render_json": "{\n \"claims\": [],\n \"error\": \"No verifiable claim detected\",\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__single_one_claim.json b/tests/fixtures/parity/expected/assess__single_one_claim.json new file mode 100644 index 0000000..6a00522 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__single_one_claim.json @@ -0,0 +1,26 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "The registry reported 4,200 filings in 2024.", + "confidence": "high", + "dissent": "The registry figure is provisional, so the 2024 total cannot yet be confirmed.", + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "The registry lists 4,200 filings for 2024 and has not revised the figure.", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "True (high) — The registry reported 4,200 filings in 2024.\n The registry lists 4,200 filings for 2024 and has not revised the figure.\n One reviewer disagreed: The registry figure is provisional, so the 2024 total cannot yet be confirmed.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The registry reported 4,200 filings in 2024.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"The registry lists 4,200 filings for 2024 and has not revised the figure.\",\n \"dissent\": \"The registry figure is provisional, so the 2024 total cannot yet be confirmed.\",\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__single_text_over_wave_more_claims.json b/tests/fixtures/parity/expected/assess__single_text_over_wave_more_claims.json new file mode 100644 index 0000000..519a6b2 --- /dev/null +++ b/tests/fixtures/parity/expected/assess__single_text_over_wave_more_claims.json @@ -0,0 +1,295 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "Claim number 0 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 1 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 2 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 3 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 4 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 5 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 6 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 7 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 8 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 9 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 10 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 11 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 12 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 13 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 14 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 15 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 16 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 17 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 18 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 19 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [ + "Claim number 20 is documented.", + "Claim number 21 is documented." + ] + }, + "render": "True (high) — Claim number 0 is documented.\nTrue (high) — Claim number 1 is documented.\nTrue (high) — Claim number 2 is documented.\nTrue (high) — Claim number 3 is documented.\nTrue (high) — Claim number 4 is documented.\nTrue (high) — Claim number 5 is documented.\nTrue (high) — Claim number 6 is documented.\nTrue (high) — Claim number 7 is documented.\nTrue (high) — Claim number 8 is documented.\nTrue (high) — Claim number 9 is documented.\nTrue (high) — Claim number 10 is documented.\nTrue (high) — Claim number 11 is documented.\nTrue (high) — Claim number 12 is documented.\nTrue (high) — Claim number 13 is documented.\nTrue (high) — Claim number 14 is documented.\nTrue (high) — Claim number 15 is documented.\nTrue (high) — Claim number 16 is documented.\nTrue (high) — Claim number 17 is documented.\nTrue (high) — Claim number 18 is documented.\nTrue (high) — Claim number 19 is documented.\n\nNot checked (2 more claims found; pass them to `lenz assess`, 20 a call):\n • Claim number 20 is documented.\n • Claim number 21 is documented.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Claim number 0 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 1 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 2 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 3 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 4 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 5 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 6 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 7 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 8 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 9 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 10 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 11 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 12 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 13 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 14 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 15 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 16 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 17 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 18 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 19 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": [\n \"Claim number 20 is documented.\",\n \"Claim number 21 is documented.\"\n ]\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__single_text_several_claims.json b/tests/fixtures/parity/expected/assess__single_text_several_claims.json new file mode 100644 index 0000000..62e14ad --- /dev/null +++ b/tests/fixtures/parity/expected/assess__single_text_several_claims.json @@ -0,0 +1,54 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "Water boils at 100 C at sea level.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "The Moon is made of cheese.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "The Pacific is the deepest ocean.", + "confidence": "medium", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Mostly True", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "True (high) — Water boils at 100 C at sea level.\nFalse (high) — The Moon is made of cheese.\nMostly True (medium) — The Pacific is the deepest ocean.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Water boils at 100 C at sea level.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"The Moon is made of cheese.\",\n \"language\": \"en\",\n \"verdict\": \"False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"The Pacific is the deepest ocean.\",\n \"language\": \"en\",\n \"verdict\": \"Mostly True\",\n \"confidence\": \"medium\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/assess__suggest_rewrite.json b/tests/fixtures/parity/expected/assess__suggest_rewrite.json new file mode 100644 index 0000000..da850bb --- /dev/null +++ b/tests/fixtures/parity/expected/assess__suggest_rewrite.json @@ -0,0 +1,40 @@ +{ + "dump": { + "candidate_claims": [], + "claims": [ + { + "candidate_claims": [], + "claim": "Venus is the closest planet to the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "Mercury, not Venus, is the closest planet to the Sun.", + "suggested_rewrite": "Mercury is the closest planet to the Sun.", + "verdict": "False", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Mercury is the closest planet to the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "Mercury is the innermost planet.", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "error_code": "", + "more_claims": [] + }, + "render": "False (high) — Venus is the closest planet to the Sun.\n Mercury, not Venus, is the closest planet to the Sun.\nTrue (high) — Mercury is the closest planet to the Sun.\n Mercury is the innermost planet.\n", + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Venus is the closest planet to the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"Mercury, not Venus, is the closest planet to the Sun.\",\n \"dissent\": null,\n \"suggested_rewrite\": \"Mercury is the closest planet to the Sun.\",\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Mercury is the closest planet to the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"Mercury is the innermost planet.\",\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/citecheck__402_no_credits.json b/tests/fixtures/parity/expected/citecheck__402_no_credits.json new file mode 100644 index 0000000..9d5ec22 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__402_no_credits.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "No remaining credits for citation checks.", + "code": "no_credits", + "cost": 1, + "credit_balance": 100, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "No remaining credits for citation checks.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "No remaining credits for citation checks.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "No remaining credits for citation checks.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 100, + "request_id": "", + "requested": null, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_empty_body.json b/tests/fixtures/parity/expected/citecheck__422_empty_body.json new file mode 100644 index 0000000..26fe6bc --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_empty_body.json @@ -0,0 +1,30 @@ +{ + "error": { + "cause": "payload: Value error, send exactly one of text and pairs", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload" + ], + "msg": "Value error, send exactly one of text and pairs" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload: Value error, send exactly one of text and pairs\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload: Value error, send exactly one of text and pairs", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload: Value error, send exactly one of text and pairs", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_invalid_doi.json b/tests/fixtures/parity/expected/citecheck__422_invalid_doi.json new file mode 100644 index 0000000..3e13201 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_invalid_doi.json @@ -0,0 +1,33 @@ +{ + "error": { + "cause": "payload.pairs.0.doi: Send the DOI alone, like 10.1038/nature12373.", + "code": "invalid_doi", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0, + "doi" + ], + "msg": "Send the DOI alone, like 10.1038/nature12373." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.pairs.0.doi: Send the DOI alone, like 10.1038/nature12373.\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0.doi: Send the DOI alone, like 10.1038/nature12373.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0.doi: Send the DOI alone, like 10.1038/nature12373.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_max_citations_over_20.json b/tests/fixtures/parity/expected/citecheck__422_max_citations_over_20.json new file mode 100644 index 0000000..aae4ebc --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_max_citations_over_20.json @@ -0,0 +1,31 @@ +{ + "error": { + "cause": "payload.max_citations: Input should be less than or equal to 20", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "max_citations" + ], + "msg": "Input should be less than or equal to 20" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.max_citations: Input should be less than or equal to 20\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.max_citations: Input should be less than or equal to 20", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.max_citations: Input should be less than or equal to 20", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_max_citations_with_pairs.json b/tests/fixtures/parity/expected/citecheck__422_max_citations_with_pairs.json new file mode 100644 index 0000000..f08b4c2 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_max_citations_with_pairs.json @@ -0,0 +1,30 @@ +{ + "error": { + "cause": "payload: Value error, max_citations goes with text: every pair is checked", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload" + ], + "msg": "Value error, max_citations goes with text: every pair is checked" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload: Value error, max_citations goes with text: every pair is checked\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload: Value error, max_citations goes with text: every pair is checked", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload: Value error, max_citations goes with text: every pair is checked", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_pair_bad_url_scheme.json b/tests/fixtures/parity/expected/citecheck__422_pair_bad_url_scheme.json new file mode 100644 index 0000000..76595fb --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_pair_bad_url_scheme.json @@ -0,0 +1,33 @@ +{ + "error": { + "cause": "payload.pairs.0.url: Value error, url: an http or https link", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0, + "url" + ], + "msg": "Value error, url: an http or https link" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.pairs.0.url: Value error, url: an http or https link\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0.url: Value error, url: an http or https link", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0.url: Value error, url: an http or https link", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_pair_no_source.json b/tests/fixtures/parity/expected/citecheck__422_pair_no_source.json new file mode 100644 index 0000000..f8ebec7 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_pair_no_source.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.pairs.0: Value error, send exactly one of url and doi", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0 + ], + "msg": "Value error, send exactly one of url and doi" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.pairs.0: Value error, send exactly one of url and doi\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0: Value error, send exactly one of url and doi", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0: Value error, send exactly one of url and doi", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_pair_url_and_doi.json b/tests/fixtures/parity/expected/citecheck__422_pair_url_and_doi.json new file mode 100644 index 0000000..f8ebec7 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_pair_url_and_doi.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.pairs.0: Value error, send exactly one of url and doi", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0 + ], + "msg": "Value error, send exactly one of url and doi" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.pairs.0: Value error, send exactly one of url and doi\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0: Value error, send exactly one of url and doi", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0: Value error, send exactly one of url and doi", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_pairs_empty.json b/tests/fixtures/parity/expected/citecheck__422_pairs_empty.json new file mode 100644 index 0000000..53538a0 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_pairs_empty.json @@ -0,0 +1,31 @@ +{ + "error": { + "cause": "payload.pairs: Value error, pairs: 1 to 20 items", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs" + ], + "msg": "Value error, pairs: 1 to 20 items" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.pairs: Value error, pairs: 1 to 20 items\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.pairs: Value error, pairs: 1 to 20 items", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.pairs: Value error, pairs: 1 to 20 items", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_pairs_over_20.json b/tests/fixtures/parity/expected/citecheck__422_pairs_over_20.json new file mode 100644 index 0000000..53538a0 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_pairs_over_20.json @@ -0,0 +1,31 @@ +{ + "error": { + "cause": "payload.pairs: Value error, pairs: 1 to 20 items", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs" + ], + "msg": "Value error, pairs: 1 to 20 items" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.pairs: Value error, pairs: 1 to 20 items\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.pairs: Value error, pairs: 1 to 20 items", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.pairs: Value error, pairs: 1 to 20 items", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_quote_not_in_statement.json b/tests/fixtures/parity/expected/citecheck__422_quote_not_in_statement.json new file mode 100644 index 0000000..96e097c --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_quote_not_in_statement.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.pairs.0: Each quote must be words of its statement.", + "code": "quote_not_in_statement", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0 + ], + "msg": "Each quote must be words of its statement." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.pairs.0: Each quote must be words of its statement.\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0: Each quote must be words of its statement.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0: Each quote must be words of its statement.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_text_and_pairs.json b/tests/fixtures/parity/expected/citecheck__422_text_and_pairs.json new file mode 100644 index 0000000..26fe6bc --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_text_and_pairs.json @@ -0,0 +1,30 @@ +{ + "error": { + "cause": "payload: Value error, send exactly one of text and pairs", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload" + ], + "msg": "Value error, send exactly one of text and pairs" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload: Value error, send exactly one of text and pairs\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload: Value error, send exactly one of text and pairs", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload: Value error, send exactly one of text and pairs", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_unknown_field.json b/tests/fixtures/parity/expected/citecheck__422_unknown_field.json new file mode 100644 index 0000000..bafff34 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_unknown_field.json @@ -0,0 +1,33 @@ +{ + "error": { + "cause": "payload.pairs.0.extra: Extra inputs are not permitted", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0, + "extra" + ], + "msg": "Extra inputs are not permitted" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.pairs.0.extra: Extra inputs are not permitted\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0.extra: Extra inputs are not permitted", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.pairs.0.extra: Extra inputs are not permitted", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__422_url_input.json b/tests/fixtures/parity/expected/citecheck__422_url_input.json new file mode 100644 index 0000000..d5d08dc --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_url_input.json @@ -0,0 +1,30 @@ +{ + "error": { + "cause": "Send the page's text, with its links, to check the sources it cites.", + "code": "url_input", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "text" + ], + "msg": "Send the page's text, with its links, to check the sources it cites." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Send the page's text, with its links, to check the sources it cites.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Send the page's text, with its links, to check the sources it cites.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Send the page's text, with its links, to check the sources it cites.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__429_citecheck_in_flight.json b/tests/fixtures/parity/expected/citecheck__429_citecheck_in_flight.json new file mode 100644 index 0000000..45b31c1 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__429_citecheck_in_flight.json @@ -0,0 +1,25 @@ +{ + "error": { + "cause": "This account already has 3 citation checks running. Wait for one to finish.", + "code": "citecheck_in_flight", + "doc_url": "https://lenz.io/docs/rate-limits", + "fix": "Wait Retry-After seconds and retry.", + "friendly_text": "This account already has 3 citation checks running. Wait for one to finish.\n Fix: Wait Retry-After seconds and retry.", + "limit": null, + "message": "This account already has 3 citation checks running. Wait for one to finish.", + "payload_json": { + "error": { + "code": "rate_limited", + "fix": "Wait Retry-After seconds and retry.", + "message": "This account already has 3 citation checks running. Wait for one to finish.", + "status": 429 + } + }, + "request_id": "", + "reset_in_seconds": null, + "retry_after": 60, + "status_code": 429, + "type": "LenzRateLimitError", + "upgrade_url": "" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__503_capacity.json b/tests/fixtures/parity/expected/citecheck__503_capacity.json new file mode 100644 index 0000000..5384727 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__503_capacity.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 90, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__503_citations_unavailable.json b/tests/fixtures/parity/expected/citecheck__503_citations_unavailable.json new file mode 100644 index 0000000..d3cc317 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__503_citations_unavailable.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Citation checking is switched off right now. Retry later; nothing was charged.", + "code": "citations_unavailable", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Citation checking is switched off right now. Retry later; nothing was charged.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Citation checking is switched off right now. Retry later; nothing was charged.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Citation checking is switched off right now. Retry later; nothing was charged.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 300, + "status_code": 503, + "type": "LenzAPIError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__get_404_not_found.json b/tests/fixtures/parity/expected/citecheck__get_404_not_found.json new file mode 100644 index 0000000..b1353bd --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_404_not_found.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Citation check not found.", + "code": "not_found", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Citation check not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Citation check not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Citation check not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__get_404_review_id.json b/tests/fixtures/parity/expected/citecheck__get_404_review_id.json new file mode 100644 index 0000000..b1353bd --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_404_review_id.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Citation check not found.", + "code": "not_found", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Citation check not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Citation check not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Citation check not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__get_410_purged.json b/tests/fixtures/parity/expected/citecheck__get_410_purged.json new file mode 100644 index 0000000..6cf59a9 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_410_purged.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "This check is no longer available: its account removes content after a set period.", + "code": "purged", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Its account's retention period removed it. A certificate issued for it is still available.", + "friendly_text": "This check is no longer available: its account removes content after a set period.\n Fix: Its account's retention period removed it. A certificate issued for it is still available.", + "message": "This check is no longer available: its account removes content after a set period.", + "payload_json": { + "error": { + "code": "gone", + "fix": "Its account's retention period removed it. A certificate issued for it is still available.", + "message": "This check is no longer available: its account removes content after a set period.", + "status": 410 + } + }, + "purged_at": "2026-10-01T09:07:00.123456Z", + "request_id": "", + "status_code": 410, + "type": "LenzGoneError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__get_checking.json b/tests/fixtures/parity/expected/citecheck__get_checking.json new file mode 100644 index 0000000..c5352dd --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_checking.json @@ -0,0 +1,100 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": null, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": null, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": null, + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": 10, + "status": "checking", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "exit_code": 2, + "render": "Citation check 8eb49302: checking — 0 credits charged\n\n2 sources cited in your draft\nLenz checks each one: does the source say what your draft says it does.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_clean.json b/tests/fixtures/parity/expected/citecheck__get_completed_clean.json new file mode 100644 index 0000000..0b78c9e --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_completed_clean.json @@ -0,0 +1,108 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "clean", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "exit_code": 0, + "render": "Citation check 8eb49302: clean — 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_doi_pair.json b/tests/fixtures/parity/expected/citecheck__get_completed_doi_pair.json new file mode 100644 index 0000000..1857179 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_completed_doi_pair.json @@ -0,0 +1,73 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at None", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": null, + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://doi.org/10.1038/nature12373", + "doi": "10.1038/nature12373", + "index": 0, + "position": null, + "quotes": [], + "reference": "10.1038/nature12373", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Diamond sensors measure heat in cells." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "clean", + "policy": { + "max_citations": 1 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 1, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 1, + "citation_limit_reached": false, + "citations_found": 1, + "citations_selected": 1 + } + }, + "exit_code": 0, + "render": "Citation check 8eb49302: clean — 1 credit charged\n\n1 source cited in your draft\n1 checked.\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_incomplete.json b/tests/fixtures/parity/expected/citecheck__get_completed_incomplete.json new file mode 100644 index 0000000..267612f --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_completed_incomplete.json @@ -0,0 +1,124 @@ +{ + "dump": { + "citation_failures": [ + { + "citation_index": 1, + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#internal", + "failure_class": "internal", + "failure_reason": "internal", + "hint": "The check failed on our side. Try again later.", + "retryable": false + }, + "reference": "https://example.org/statement" + } + ], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#internal", + "failure_class": "internal", + "failure_reason": "internal", + "hint": "The check failed on our side. Try again later.", + "retryable": false + }, + "hint": "The check failed on our side. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": null, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "incomplete", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 1, + "failed": 1, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "exit_code": 2, + "render": "Citation check 8eb49302: incomplete — 1 credit charged\n\n2 sources cited in your draft\n1 checked. 1 could not be checked.\n\n[source 2/2] Could not be checked this time.\n https://example.org/statement\n The check failed on our side. Try again later.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_issues_found.json b/tests/fixtures/parity/expected/citecheck__get_completed_issues_found.json new file mode 100644 index 0000000..0bee066 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_completed_issues_found.json @@ -0,0 +1,138 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": null, + "finding": "contradicted", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "rationale": "The page states it.", + "reference": "a report from the ministry", + "snippet": "The source says so.", + "source": "support", + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "contradicted", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "contradicted", + "is_issue": true, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "issues_found", + "policy": { + "max_citations": 20 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 1, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "exit_code": 1, + "render": "Citation check 8eb49302: issues found — 2 credits charged\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_partly_supported.json b/tests/fixtures/parity/expected/citecheck__get_completed_partly_supported.json new file mode 100644 index 0000000..3d302ef --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_completed_partly_supported.json @@ -0,0 +1,108 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "partly_supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": { + "finding": "partly_supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "clean", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "exit_code": 0, + "render": "Citation check 8eb49302: clean — 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_unchecked.json b/tests/fixtures/parity/expected/citecheck__get_completed_unchecked.json new file mode 100644 index 0000000..4e9a931 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_completed_unchecked.json @@ -0,0 +1,108 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": null, + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "unchecked", + "unchecked_reason": "no_text" + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": { + "finding": "unchecked", + "is_issue": false, + "source": null + }, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": null, + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "unchecked", + "unchecked_reason": "no_text" + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": { + "finding": "unchecked", + "is_issue": false, + "source": null + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "unchecked", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 0, + "unchecked": 2 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "exit_code": 2, + "render": "Citation check 8eb49302: unchecked — 0 credits charged\n\n2 sources cited in your draft\n2 could not be checked.\n\nCheck these by hand (Lenz could not check them)\n[source 1/2] The page gave no text to read.\n https://example.gov/report-2024\n[source 2/2] The page gave no text to read.\n https://example.org/statement\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_failed_no_citations.json b/tests/fixtures/parity/expected/citecheck__get_failed_no_citations.json new file mode 100644 index 0000000..2fcc755 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_failed_no_citations.json @@ -0,0 +1,39 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_citations", + "hint": "The text has no citation to check: no link, DOI or numbered reference with one.", + "retryable": false + }, + "language": "en", + "more_citations": null, + "outcome": "unchecked", + "policy": { + "max_citations": 20 + }, + "poll_after_seconds": null, + "status": "failed", + "summary": { + "citation_checks": null, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null + } + }, + "exit_code": 2, + "render": "Citation check 8eb49302: unchecked — 0 credits charged\nFailed: The text has no citation to check: no link, DOI or numbered reference with one.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_failed_upstream_unavailable.json b/tests/fixtures/parity/expected/citecheck__get_failed_upstream_unavailable.json new file mode 100644 index 0000000..81d92d8 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_failed_upstream_unavailable.json @@ -0,0 +1,145 @@ +{ + "dump": { + "citation_failures": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "Lenz could not reach this source just now. Try again later.", + "retryable": true + }, + "reference": "a report from the ministry" + }, + { + "citation_index": 1, + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "Lenz could not reach this source just now. Try again later.", + "retryable": true + }, + "reference": "a statement" + } + ], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "Lenz could not reach this source just now. Try again later.", + "retryable": true + }, + "hint": "Lenz could not reach this source just now. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "a report from the ministry", + "result": null, + "statement": null + }, + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "Lenz could not reach this source just now. Try again later.", + "retryable": true + }, + "hint": "Lenz could not reach this source just now. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "a statement", + "result": null, + "statement": null + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": null, + "retryable": true + }, + "language": "en", + "more_citations": [], + "outcome": "unchecked", + "policy": { + "max_citations": 20 + }, + "poll_after_seconds": null, + "status": "failed", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 2, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "exit_code": 2, + "render": "Citation check 8eb49302: unchecked — 0 credits charged\nFailed: upstream_unavailable\n\n2 sources cited in your draft\n2 could not be checked.\n\n[source 1/2] Could not be checked this time.\n https://example.gov/report-2024\n Lenz could not reach this source just now. Try again later.\n\n[source 2/2] Could not be checked this time.\n https://example.org/statement\n Lenz could not reach this source just now. Try again later.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_queued.json b/tests/fixtures/parity/expected/citecheck__get_queued.json new file mode 100644 index 0000000..402fbe9 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_queued.json @@ -0,0 +1,69 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": null, + "statement": "Unemployment fell to 4.1% in 2024." + } + ], + "citecheck_id": "8eb49302", + "completed_at": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": null, + "policy": { + "max_citations": 1 + }, + "poll_after_seconds": 10, + "status": "queued", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 1, + "citation_limit_reached": false, + "citations_found": 1, + "citations_selected": 1 + } + }, + "exit_code": 2, + "render": "Citation check 8eb49302: queued — 0 credits charged\n\n1 source cited in your draft\nLenz checks each one: does the source say what your draft says it does.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_text_limit_exactly_at_limit.json b/tests/fixtures/parity/expected/citecheck__get_text_limit_exactly_at_limit.json new file mode 100644 index 0000000..174ac25 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_text_limit_exactly_at_limit.json @@ -0,0 +1,116 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "clean", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "exit_code": 0, + "render": "Citation check 8eb49302: clean — 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__get_text_limit_reached.json b/tests/fixtures/parity/expected/citecheck__get_text_limit_reached.json new file mode 100644 index 0000000..b97967a --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_text_limit_reached.json @@ -0,0 +1,90 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "language": "en", + "more_citations": [ + { + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "reference": "a statement", + "sentence": "The agency said so in a statement." + } + ], + "outcome": "clean", + "policy": { + "max_citations": 1 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 1, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 1, + "citation_limit_reached": true, + "citations_found": 2, + "citations_selected": 1 + } + }, + "exit_code": 0, + "render": "Citation check 8eb49302: clean — 1 credit charged\n\n2 sources cited in your draft\n1 checked.\n1 more was not checked: one check covers 1.\n\nNo issues.\n\n1 more citation was found but not checked.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__idempotency_body_mismatch_422.json b/tests/fixtures/parity/expected/citecheck__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..5de20e3 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__idempotency_body_mismatch_422.json @@ -0,0 +1,29 @@ +{ + "error": { + "cause": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "header" + ], + "msg": "Idempotency-Key reused with a different request body." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Idempotency-Key reused with a different request body.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__idempotency_conflict_409.json b/tests/fixtures/parity/expected/citecheck__idempotency_conflict_409.json new file mode 100644 index 0000000..f674e5f --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__idempotency_conflict_409.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "A check with this Idempotency-Key is still being created. Retry shortly.", + "code": "idempotency_conflict", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "A check with this Idempotency-Key is still being created. Retry shortly.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "A check with this Idempotency-Key is still being created. Retry shortly.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "A check with this Idempotency-Key is still being created. Retry shortly.", + "status": 409 + } + }, + "request_id": "", + "status_code": 409, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__idempotent_replay_202.json b/tests/fixtures/parity/expected/citecheck__idempotent_replay_202.json new file mode 100644 index 0000000..6ebcb9b --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__idempotent_replay_202.json @@ -0,0 +1,7 @@ +{ + "dump": { + "citecheck_id": "8eb49302", + "status": "queued" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__receipt_202.json b/tests/fixtures/parity/expected/citecheck__receipt_202.json new file mode 100644 index 0000000..6ebcb9b --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__receipt_202.json @@ -0,0 +1,7 @@ +{ + "dump": { + "citecheck_id": "8eb49302", + "status": "queued" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/citecheck__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/expected/citecheck__receipt_202_empty_webhook_url.json new file mode 100644 index 0000000..6ebcb9b --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__receipt_202_empty_webhook_url.json @@ -0,0 +1,7 @@ +{ + "dump": { + "citecheck_id": "8eb49302", + "status": "queued" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/errors__ask_bad_language.json b/tests/fixtures/parity/expected/errors__ask_bad_language.json new file mode 100644 index 0000000..81b1bdf --- /dev/null +++ b/tests/fixtures/parity/expected/errors__ask_bad_language.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/errors__ask_empty_message.json b/tests/fixtures/parity/expected/errors__ask_empty_message.json new file mode 100644 index 0000000..817db2b --- /dev/null +++ b/tests/fixtures/parity/expected/errors__ask_empty_message.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Message cannot be empty.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Message cannot be empty.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Message cannot be empty.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Message cannot be empty.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/errors__ask_failed.json b/tests/fixtures/parity/expected/errors__ask_failed.json new file mode 100644 index 0000000..4aa0601 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__ask_failed.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "The chat model could not answer this question.", + "code": "ask_failed", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "The chat model could not answer this question.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "The chat model could not answer this question.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "The chat model could not answer this question.", + "status": 502 + } + }, + "request_id": "", + "retry_after": null, + "status_code": 502, + "type": "LenzAPIError" + } +} diff --git a/tests/fixtures/parity/expected/errors__ask_idempotency_key_reused.json b/tests/fixtures/parity/expected/errors__ask_idempotency_key_reused.json new file mode 100644 index 0000000..989af48 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__ask_idempotency_key_reused.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Idempotency-Key reused with a different request body.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Idempotency-Key reused with a different request body.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/errors__ask_message_too_long.json b/tests/fixtures/parity/expected/errors__ask_message_too_long.json new file mode 100644 index 0000000..8345a29 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__ask_message_too_long.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Message too long (max 500 characters).", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Message too long (max 500 characters).\n Fix: Check the request body against the OpenAPI spec.", + "message": "Message too long (max 500 characters).", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Message too long (max 500 characters).", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/errors__ask_not_completed.json b/tests/fixtures/parity/expected/errors__ask_not_completed.json new file mode 100644 index 0000000..cfdc7c3 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__ask_not_completed.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Ask is only available for completed verifications.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Ask is only available for completed verifications.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Ask is only available for completed verifications.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Ask is only available for completed verifications.", + "status": 400 + } + }, + "request_id": "", + "status_code": 400, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__ask_reset_not_found.json b/tests/fixtures/parity/expected/errors__ask_reset_not_found.json new file mode 100644 index 0000000..956264c --- /dev/null +++ b/tests/fixtures/parity/expected/errors__ask_reset_not_found.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Verification not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Verification not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Verification not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Verification not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__ask_send_not_found.json b/tests/fixtures/parity/expected/errors__ask_send_not_found.json new file mode 100644 index 0000000..956264c --- /dev/null +++ b/tests/fixtures/parity/expected/errors__ask_send_not_found.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Verification not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Verification not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Verification not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Verification not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_bad_key.json b/tests/fixtures/parity/expected/errors__auth_bad_key.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_bad_key.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_basic_scheme.json b/tests/fixtures/parity/expected/errors__auth_basic_scheme.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_basic_scheme.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_insufficient_scope.json b/tests/fixtures/parity/expected/errors__auth_insufficient_scope.json new file mode 100644 index 0000000..567f3ae --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_insufficient_scope.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "The access token does not allow this call.", + "code": "insufficient_scope", + "doc_url": "https://lenz.io/docs/auth", + "fix": "This key doesn't have access to that resource.", + "friendly_text": "The access token does not allow this call.\n Fix: This key doesn't have access to that resource.", + "message": "The access token does not allow this call.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "This key doesn't have access to that resource.", + "message": "The access token does not allow this call.", + "status": 403 + } + }, + "request_id": "", + "status_code": 403, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_missing_bearer.json b/tests/fixtures/parity/expected/errors__auth_missing_bearer.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_missing_bearer.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_missing_bearer_post.json b/tests/fixtures/parity/expected/errors__auth_missing_bearer_post.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_missing_bearer_post.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_private_claim_ask_history.json b/tests/fixtures/parity/expected/errors__auth_private_claim_ask_history.json new file mode 100644 index 0000000..a29a3fb --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_private_claim_ask_history.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "This report is private.", + "code": "private_claim", + "doc_url": "https://lenz.io/docs/auth", + "fix": "This key doesn't have access to that resource.", + "friendly_text": "This report is private.\n Fix: This key doesn't have access to that resource.", + "message": "This report is private.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "This key doesn't have access to that resource.", + "message": "This report is private.", + "status": 403 + } + }, + "request_id": "", + "status_code": 403, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_private_claim_ask_send.json b/tests/fixtures/parity/expected/errors__auth_private_claim_ask_send.json new file mode 100644 index 0000000..a29a3fb --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_private_claim_ask_send.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "This report is private.", + "code": "private_claim", + "doc_url": "https://lenz.io/docs/auth", + "fix": "This key doesn't have access to that resource.", + "friendly_text": "This report is private.\n Fix: This key doesn't have access to that resource.", + "message": "This report is private.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "This key doesn't have access to that resource.", + "message": "This report is private.", + "status": 403 + } + }, + "request_id": "", + "status_code": 403, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_unrecognised_prefix.json b/tests/fixtures/parity/expected/errors__auth_unrecognised_prefix.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_unrecognised_prefix.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__http_error_library_bad_curated.json b/tests/fixtures/parity/expected/errors__http_error_library_bad_curated.json new file mode 100644 index 0000000..9eb06ac --- /dev/null +++ b/tests/fixtures/parity/expected/errors__http_error_library_bad_curated.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Invalid curated list(s). Allowed: trivia", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Invalid curated list(s). Allowed: trivia\n Fix: Check the request body against the OpenAPI spec.", + "message": "Invalid curated list(s). Allowed: trivia", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Invalid curated list(s). Allowed: trivia", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/errors__http_error_library_bad_verdict.json b/tests/fixtures/parity/expected/errors__http_error_library_bad_verdict.json new file mode 100644 index 0000000..2dce420 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__http_error_library_bad_verdict.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True\n Fix: Check the request body against the OpenAPI spec.", + "message": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/errors__not_found_ask.json b/tests/fixtures/parity/expected/errors__not_found_ask.json new file mode 100644 index 0000000..956264c --- /dev/null +++ b/tests/fixtures/parity/expected/errors__not_found_ask.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Verification not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Verification not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Verification not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Verification not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__not_found_route.json b/tests/fixtures/parity/expected/errors__not_found_route.json new file mode 100644 index 0000000..8c9406d --- /dev/null +++ b/tests/fixtures/parity/expected/errors__not_found_route.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 404", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 404\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 404", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 404", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__not_found_verification.json b/tests/fixtures/parity/expected/errors__not_found_verification.json new file mode 100644 index 0000000..41573fe --- /dev/null +++ b/tests/fixtures/parity/expected/errors__not_found_verification.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__not_found_verify_status.json b/tests/fixtures/parity/expected/errors__not_found_verify_status.json new file mode 100644 index 0000000..68a468c --- /dev/null +++ b/tests/fixtures/parity/expected/errors__not_found_verify_status.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Task not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Task not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Task not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Task not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__payment_required_ask.json b/tests/fixtures/parity/expected/errors__payment_required_ask.json new file mode 100644 index 0000000..e519757 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__payment_required_ask.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "No remaining ask credits.", + "code": "no_credits", + "cost": 1, + "credit_balance": 0, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "No remaining ask credits.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "No remaining ask credits.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "No remaining ask credits.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 0, + "request_id": "", + "requested": null, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/errors__payment_required_assess.json b/tests/fixtures/parity/expected/errors__payment_required_assess.json new file mode 100644 index 0000000..732bc39 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__payment_required_assess.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "No remaining /assess units.", + "code": "no_credits", + "cost": 1, + "credit_balance": 0, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "No remaining /assess units.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "No remaining /assess units.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "No remaining /assess units.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 0, + "request_id": "", + "requested": null, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/errors__payment_required_verify.json b/tests/fixtures/parity/expected/errors__payment_required_verify.json new file mode 100644 index 0000000..f3a12a3 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__payment_required_verify.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "No remaining claim checks.", + "code": "no_credits", + "cost": 10, + "credit_balance": 0, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "No remaining claim checks.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "No remaining claim checks.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "No remaining claim checks.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 0, + "request_id": "", + "requested": null, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/errors__payment_required_verify_batch_short.json b/tests/fixtures/parity/expected/errors__payment_required_verify_batch_short.json new file mode 100644 index 0000000..4506242 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__payment_required_verify_batch_short.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "Insufficient credits for batch.", + "code": "no_credits", + "cost": 50, + "credit_balance": 30, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "Insufficient credits for batch.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "Insufficient credits for batch.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "Insufficient credits for batch.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 3, + "request_id": "", + "requested": 5, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/errors__rate_limited_extract.json b/tests/fixtures/parity/expected/errors__rate_limited_extract.json new file mode 100644 index 0000000..336a59b --- /dev/null +++ b/tests/fixtures/parity/expected/errors__rate_limited_extract.json @@ -0,0 +1,25 @@ +{ + "error": { + "cause": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "code": "extract_daily_limit", + "doc_url": "https://lenz.io/docs/rate-limits", + "fix": "Wait Retry-After seconds and retry.", + "friendly_text": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.\n Fix: Wait Retry-After seconds and retry.", + "limit": 1000, + "message": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "payload_json": { + "error": { + "code": "rate_limited", + "fix": "Wait Retry-After seconds and retry.", + "message": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "status": 429 + } + }, + "request_id": "", + "reset_in_seconds": 900, + "retry_after": 900, + "status_code": 429, + "type": "LenzRateLimitError", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/expected/errors__service_unavailable_ask.json b/tests/fixtures/parity/expected/errors__service_unavailable_ask.json new file mode 100644 index 0000000..c9e8778 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__service_unavailable_ask.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "The chat model is unavailable right now. Retry shortly.", + "code": "upstream_unavailable", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "The chat model is unavailable right now. Retry shortly.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "The chat model is unavailable right now. Retry shortly.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "The chat model is unavailable right now. Retry shortly.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 90, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/errors__service_unavailable_capacity.json b/tests/fixtures/parity/expected/errors__service_unavailable_capacity.json new file mode 100644 index 0000000..b628bec --- /dev/null +++ b/tests/fixtures/parity/expected/errors__service_unavailable_capacity.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 60, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/errors__validation_malformed_json.json b/tests/fixtures/parity/expected/errors__validation_malformed_json.json new file mode 100644 index 0000000..4321bb7 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__validation_malformed_json.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Cannot parse request body (Expecting property name enclosed in double quotes: line 1 column 2 (char 1))", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Cannot parse request body (Expecting property name enclosed in double quotes: line 1 column 2 (char 1))\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Cannot parse request body (Expecting property name enclosed in double quotes: line 1 column 2 (char 1))", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Cannot parse request body (Expecting property name enclosed in double quotes: line 1 column 2 (char 1))", + "status": 400 + } + }, + "request_id": "", + "status_code": 400, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__validation_missing_field_hint.json b/tests/fixtures/parity/expected/errors__validation_missing_field_hint.json new file mode 100644 index 0000000..6760d1a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__validation_missing_field_hint.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/errors__validation_unknown_field_only.json b/tests/fixtures/parity/expected/errors__validation_unknown_field_only.json new file mode 100644 index 0000000..6760d1a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__validation_unknown_field_only.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/errors__validation_unknown_field_wrong_type.json b/tests/fixtures/parity/expected/errors__validation_unknown_field_wrong_type.json new file mode 100644 index 0000000..7713b4c --- /dev/null +++ b/tests/fixtures/parity/expected/errors__validation_unknown_field_wrong_type.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'list_type', 'loc': ['body', 'payload', 'claims'], 'msg': 'Input should be a valid list'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "claims" + ], + "msg": "Input should be a valid list", + "type": "list_type" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'list_type', 'loc': ['body', 'payload', 'claims'], 'msg': 'Input should be a valid list'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'list_type', 'loc': ['body', 'payload', 'claims'], 'msg': 'Input should be a valid list'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'list_type', 'loc': ['body', 'payload', 'claims'], 'msg': 'Input should be a valid list'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/errors__validation_wrong_type.json b/tests/fixtures/parity/expected/errors__validation_wrong_type.json new file mode 100644 index 0000000..bf16952 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__validation_wrong_type.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'string_type', 'loc': ['body', 'payload', 'text'], 'msg': 'Input should be a valid string'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Input should be a valid string", + "type": "string_type" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'string_type', 'loc': ['body', 'payload', 'text'], 'msg': 'Input should be a valid string'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'string_type', 'loc': ['body', 'payload', 'text'], 'msg': 'Input should be a valid string'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'string_type', 'loc': ['body', 'payload', 'text'], 'msg': 'Input should be a valid string'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/extract__401_no_key.json b/tests/fixtures/parity/expected/extract__401_no_key.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/extract__401_no_key.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/extract__422_blank_text.json b/tests/fixtures/parity/expected/extract__422_blank_text.json new file mode 100644 index 0000000..872ff11 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__422_blank_text.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Text is required.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Text is required.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Text is required.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Text is required.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/extract__422_focus_too_long.json b/tests/fixtures/parity/expected/extract__422_focus_too_long.json new file mode 100644 index 0000000..e658cd0 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__422_focus_too_long.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "focus must be at most 300 characters of plain text.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "focus must be at most 300 characters of plain text.\n Fix: Check the request body against the OpenAPI spec.", + "message": "focus must be at most 300 characters of plain text.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "focus must be at most 300 characters of plain text.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/extract__422_missing_text.json b/tests/fixtures/parity/expected/extract__422_missing_text.json new file mode 100644 index 0000000..6760d1a --- /dev/null +++ b/tests/fixtures/parity/expected/extract__422_missing_text.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/extract__429_daily_limit.json b/tests/fixtures/parity/expected/extract__429_daily_limit.json new file mode 100644 index 0000000..fe317f9 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__429_daily_limit.json @@ -0,0 +1,25 @@ +{ + "error": { + "cause": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "code": "extract_daily_limit", + "doc_url": "https://lenz.io/docs/rate-limits", + "fix": "Wait Retry-After seconds and retry.", + "friendly_text": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.\n Fix: Wait Retry-After seconds and retry.", + "limit": 1000, + "message": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "payload_json": { + "error": { + "code": "rate_limited", + "fix": "Wait Retry-After seconds and retry.", + "message": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "status": 429 + } + }, + "request_id": "", + "reset_in_seconds": 3600, + "retry_after": 3600, + "status_code": 429, + "type": "LenzRateLimitError", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/expected/extract__502_extraction_failed.json b/tests/fixtures/parity/expected/extract__502_extraction_failed.json new file mode 100644 index 0000000..7ce9984 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__502_extraction_failed.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Extraction failed", + "code": "extraction_failed", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Extraction failed\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Extraction failed", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Extraction failed", + "status": 502 + } + }, + "request_id": "", + "retry_after": null, + "status_code": 502, + "type": "LenzAPIError" + } +} diff --git a/tests/fixtures/parity/expected/extract__503_upstream_unavailable.json b/tests/fixtures/parity/expected/extract__503_upstream_unavailable.json new file mode 100644 index 0000000..7553c4c --- /dev/null +++ b/tests/fixtures/parity/expected/extract__503_upstream_unavailable.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Our model providers are temporarily unavailable. Please retry shortly.", + "code": "upstream_unavailable", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Our model providers are temporarily unavailable. Please retry shortly.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Our model providers are temporarily unavailable. Please retry shortly.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Our model providers are temporarily unavailable. Please retry shortly.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 90, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/extract__focus_ready.json b/tests/fixtures/parity/expected/extract__focus_ready.json new file mode 100644 index 0000000..0b529b1 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__focus_ready.json @@ -0,0 +1,24 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Beta fell 3% last year.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "Claim: Beta fell 3% last year.\n\nVerify it: lenz verify \"Beta fell 3% last year.\"\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Beta fell 3% last year.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__idempotency_body_mismatch_422.json b/tests/fixtures/parity/expected/extract__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..989af48 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__idempotency_body_mismatch_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Idempotency-Key reused with a different request body.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Idempotency-Key reused with a different request body.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/extract__idempotency_replay.json b/tests/fixtures/parity/expected/extract__idempotency_replay.json new file mode 100644 index 0000000..df61eab --- /dev/null +++ b/tests/fixtures/parity/expected/extract__idempotency_replay.json @@ -0,0 +1,27 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n 2. Beta fell 3% last year.\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__locate_all_dropped.json b/tests/fixtures/parity/expected/extract__locate_all_dropped.json new file mode 100644 index 0000000..c015827 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__locate_all_dropped.json @@ -0,0 +1,24 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "", + "domain": "", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [], + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "not_a_claim" + }, + "render": "No verifiable claim found in that text.\n", + "render_json": "{\n \"status\": \"not_a_claim\",\n \"claim\": \"\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": []\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__locate_failed.json b/tests/fixtures/parity/expected/extract__locate_failed.json new file mode 100644 index 0000000..df61eab --- /dev/null +++ b/tests/fixtures/parity/expected/extract__locate_failed.json @@ -0,0 +1,27 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n 2. Beta fell 3% last year.\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__locate_leading_whitespace.json b/tests/fixtures/parity/expected/extract__locate_leading_whitespace.json new file mode 100644 index 0000000..a007c40 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__locate_leading_whitespace.json @@ -0,0 +1,35 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "end": 25, + "start": 3, + "text": "Alpha rose 5% in 2024." + } + ] + } + ], + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "Claim: Alpha rose 5% in 2024.\n at 3-25: \"Alpha rose 5% in 2024.\"\n\nVerify it: lenz verify \"Alpha rose 5% in 2024.\"\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": 3,\n \"end\": 25,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n }\n ]\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__locate_some_dropped.json b/tests/fixtures/parity/expected/extract__locate_some_dropped.json new file mode 100644 index 0000000..80d25e1 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__locate_some_dropped.json @@ -0,0 +1,48 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "end": 22, + "start": 0, + "text": "Alpha rose 5% in 2024." + } + ] + }, + { + "claim": "Beta fell 3% last year.", + "positions": [ + { + "end": 46, + "start": 23, + "text": "Beta fell 3% last year." + } + ] + } + ], + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n at 0-22: \"Alpha rose 5% in 2024.\"\n 2. Beta fell 3% last year.\n at 23-46: \"Beta fell 3% last year.\"\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": 0,\n \"end\": 22,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n },\n {\n \"claim\": \"Beta fell 3% last year.\",\n \"positions\": [\n {\n \"start\": 23,\n \"end\": 46,\n \"text\": \"Beta fell 3% last year.\"\n }\n ]\n }\n ]\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__locate_true.json b/tests/fixtures/parity/expected/extract__locate_true.json new file mode 100644 index 0000000..80d25e1 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__locate_true.json @@ -0,0 +1,48 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "end": 22, + "start": 0, + "text": "Alpha rose 5% in 2024." + } + ] + }, + { + "claim": "Beta fell 3% last year.", + "positions": [ + { + "end": 46, + "start": 23, + "text": "Beta fell 3% last year." + } + ] + } + ], + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n at 0-22: \"Alpha rose 5% in 2024.\"\n 2. Beta fell 3% last year.\n at 23-46: \"Beta fell 3% last year.\"\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": 0,\n \"end\": 22,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n },\n {\n \"claim\": \"Beta fell 3% last year.\",\n \"positions\": [\n {\n \"start\": 23,\n \"end\": 46,\n \"text\": \"Beta fell 3% last year.\"\n }\n ]\n }\n ]\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__no_match_with_focus.json b/tests/fixtures/parity/expected/extract__no_match_with_focus.json new file mode 100644 index 0000000..8097b22 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__no_match_with_focus.json @@ -0,0 +1,15 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "", + "domain": "", + "identified_claims": [], + "key_entities": [], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "", + "status": "no_match" + }, + "render": "No claims matched the focus.\n", + "render_json": "{\n \"status\": \"no_match\",\n \"claim\": \"\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"\",\n \"key_entities\": [],\n \"presumed_intent\": \"\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__not_a_claim.json b/tests/fixtures/parity/expected/extract__not_a_claim.json new file mode 100644 index 0000000..1e7d7fe --- /dev/null +++ b/tests/fixtures/parity/expected/extract__not_a_claim.json @@ -0,0 +1,15 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "", + "domain": "", + "identified_claims": [], + "key_entities": [], + "locations": null, + "original_input": "What a lovely day.", + "presumed_intent": "", + "status": "not_a_claim" + }, + "render": "No verifiable claim found in that text.\n", + "render_json": "{\n \"status\": \"not_a_claim\",\n \"claim\": \"\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"\",\n \"key_entities\": [],\n \"presumed_intent\": \"\",\n \"original_input\": \"What a lovely day.\",\n \"locations\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__not_a_claim_beside_claims.json b/tests/fixtures/parity/expected/extract__not_a_claim_beside_claims.json new file mode 100644 index 0000000..05550a3 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__not_a_claim_beside_claims.json @@ -0,0 +1,24 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024.", + "presumed_intent": "Verify reported figures", + "status": "not_a_claim" + }, + "render": "Claim: Alpha rose 5% in 2024.\n\nVerify it: lenz verify \"Alpha rose 5% in 2024.\"\n", + "render_json": "{\n \"status\": \"not_a_claim\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024.\",\n \"locations\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__ready_one_claim.json b/tests/fixtures/parity/expected/extract__ready_one_claim.json new file mode 100644 index 0000000..e65da3c --- /dev/null +++ b/tests/fixtures/parity/expected/extract__ready_one_claim.json @@ -0,0 +1,24 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "Claim: Alpha rose 5% in 2024.\n\nVerify it: lenz verify \"Alpha rose 5% in 2024.\"\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024.\",\n \"locations\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__ready_several_claims.json b/tests/fixtures/parity/expected/extract__ready_several_claims.json new file mode 100644 index 0000000..df61eab --- /dev/null +++ b/tests/fixtures/parity/expected/extract__ready_several_claims.json @@ -0,0 +1,27 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n 2. Beta fell 3% last year.\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__replay_later_200.json b/tests/fixtures/parity/expected/extract__replay_later_200.json new file mode 100644 index 0000000..df61eab --- /dev/null +++ b/tests/fixtures/parity/expected/extract__replay_later_200.json @@ -0,0 +1,27 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n 2. Beta fell 3% last year.\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/extract__url_fetch_502_unreadable.json b/tests/fixtures/parity/expected/extract__url_fetch_502_unreadable.json new file mode 100644 index 0000000..7ce9984 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__url_fetch_502_unreadable.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Extraction failed", + "code": "extraction_failed", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Extraction failed\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Extraction failed", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Extraction failed", + "status": 502 + } + }, + "request_id": "", + "retry_after": null, + "status_code": 502, + "type": "LenzAPIError" + } +} diff --git a/tests/fixtures/parity/expected/extract__url_fetch_503_upstream.json b/tests/fixtures/parity/expected/extract__url_fetch_503_upstream.json new file mode 100644 index 0000000..01e9faf --- /dev/null +++ b/tests/fixtures/parity/expected/extract__url_fetch_503_upstream.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "The page could not be read right now. Please retry shortly.", + "code": "upstream_unavailable", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "The page could not be read right now. Please retry shortly.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "The page could not be read right now. Please retry shortly.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "The page could not be read right now. Please retry shortly.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 90, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/extract__url_input_located.json b/tests/fixtures/parity/expected/extract__url_input_located.json new file mode 100644 index 0000000..ca78e96 --- /dev/null +++ b/tests/fixtures/parity/expected/extract__url_input_located.json @@ -0,0 +1,35 @@ +{ + "dump": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "end": null, + "start": null, + "text": "Alpha rose 5% in 2024." + } + ] + } + ], + "original_input": "https://example.com/an-article", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, + "render": "Claim: Alpha rose 5% in 2024.\n at \"Alpha rose 5% in 2024.\"\n\nVerify it: lenz verify \"Alpha rose 5% in 2024.\"\n", + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"https://example.com/an-article\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": null,\n \"end\": null,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n }\n ]\n}\n" +} diff --git a/tests/fixtures/parity/expected/review__401_no_credentials.json b/tests/fixtures/parity/expected/review__401_no_credentials.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/review__401_no_credentials.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/review__402_no_credits.json b/tests/fixtures/parity/expected/review__402_no_credits.json new file mode 100644 index 0000000..6f8a31c --- /dev/null +++ b/tests/fixtures/parity/expected/review__402_no_credits.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "No remaining credits to assess the draft.", + "code": "no_credits", + "cost": 1, + "credit_balance": 100, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "No remaining credits to assess the draft.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "No remaining credits to assess the draft.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "No remaining credits to assess the draft.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 100, + "request_id": "", + "requested": null, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/review__402_no_credits_exhausted.json b/tests/fixtures/parity/expected/review__402_no_credits_exhausted.json new file mode 100644 index 0000000..c98a94a --- /dev/null +++ b/tests/fixtures/parity/expected/review__402_no_credits_exhausted.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "No remaining credits to assess the draft.", + "code": "no_credits", + "cost": 1, + "credit_balance": 0, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "No remaining credits to assess the draft.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "No remaining credits to assess the draft.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "No remaining credits to assess the draft.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 0, + "request_id": "", + "requested": null, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/review__422_blank_text.json b/tests/fixtures/parity/expected/review__422_blank_text.json new file mode 100644 index 0000000..2f05a24 --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_blank_text.json @@ -0,0 +1,30 @@ +{ + "error": { + "cause": "text: send the draft, or one public http(s) URL.", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "text" + ], + "msg": "text: send the draft, or one public http(s) URL." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "text: send the draft, or one public http(s) URL.\n Fix: Check the request body against the OpenAPI spec.", + "message": "text: send the draft, or one public http(s) URL.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "text: send the draft, or one public http(s) URL.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_citations_options_object.json b/tests/fixtures/parity/expected/review__422_citations_options_object.json new file mode 100644 index 0000000..eb82b86 --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_citations_options_object.json @@ -0,0 +1,31 @@ +{ + "error": { + "cause": "payload.citations: Extra inputs are not permitted", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "citations" + ], + "msg": "Extra inputs are not permitted" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.citations: Extra inputs are not permitted\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.citations: Extra inputs are not permitted", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.citations: Extra inputs are not permitted", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_depth_unknown.json b/tests/fixtures/parity/expected/review__422_depth_unknown.json new file mode 100644 index 0000000..c4709d8 --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_depth_unknown.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.escalate.depth: Input should be 'standard' or 'low'", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "depth" + ], + "msg": "Input should be 'standard' or 'low'" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.escalate.depth: Input should be 'standard' or 'low'\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.escalate.depth: Input should be 'standard' or 'low'", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.escalate.depth: Input should be 'standard' or 'low'", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_invalid_confidence_band.json b/tests/fixtures/parity/expected/review__422_invalid_confidence_band.json new file mode 100644 index 0000000..6d1bc1b --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_invalid_confidence_band.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.escalate.confidence: Unknown confidence band 'certain'; use low, medium or high.", + "code": "invalid_confidence_band", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "confidence" + ], + "msg": "Unknown confidence band 'certain'; use low, medium or high." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.escalate.confidence: Unknown confidence band 'certain'; use low, medium or high.\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.escalate.confidence: Unknown confidence band 'certain'; use low, medium or high.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.escalate.confidence: Unknown confidence band 'certain'; use low, medium or high.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_invalid_verdict_label.json b/tests/fixtures/parity/expected/review__422_invalid_verdict_label.json new file mode 100644 index 0000000..27f708f --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_invalid_verdict_label.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.escalate.verdicts: Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True.", + "code": "invalid_verdict_label", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "verdicts" + ], + "msg": "Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.escalate.verdicts: Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True.\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.escalate.verdicts: Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.escalate.verdicts: Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_max_assessments_negative.json b/tests/fixtures/parity/expected/review__422_max_assessments_negative.json new file mode 100644 index 0000000..190d110 --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_max_assessments_negative.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.escalate.max_assessments: Input should be greater than or equal to 0", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_assessments" + ], + "msg": "Input should be greater than or equal to 0" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.escalate.max_assessments: Input should be greater than or equal to 0\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.escalate.max_assessments: Input should be greater than or equal to 0", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.escalate.max_assessments: Input should be greater than or equal to 0", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_max_citations_negative.json b/tests/fixtures/parity/expected/review__422_max_citations_negative.json new file mode 100644 index 0000000..cf456cf --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_max_citations_negative.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.escalate.max_citations: Input should be greater than or equal to 0", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_citations" + ], + "msg": "Input should be greater than or equal to 0" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.escalate.max_citations: Input should be greater than or equal to 0\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.escalate.max_citations: Input should be greater than or equal to 0", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.escalate.max_citations: Input should be greater than or equal to 0", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_max_citations_too_big.json b/tests/fixtures/parity/expected/review__422_max_citations_too_big.json new file mode 100644 index 0000000..4888a1c --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_max_citations_too_big.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.escalate.max_citations: Input should be less than or equal to 20", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_citations" + ], + "msg": "Input should be less than or equal to 20" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.escalate.max_citations: Input should be less than or equal to 20\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.escalate.max_citations: Input should be less than or equal to 20", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.escalate.max_citations: Input should be less than or equal to 20", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_max_verifications_too_big.json b/tests/fixtures/parity/expected/review__422_max_verifications_too_big.json new file mode 100644 index 0000000..88ecb6e --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_max_verifications_too_big.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.escalate.max_verifications: Input should be less than or equal to 20", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_verifications" + ], + "msg": "Input should be less than or equal to 20" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.escalate.max_verifications: Input should be less than or equal to 20\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.escalate.max_verifications: Input should be less than or equal to 20", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.escalate.max_verifications: Input should be less than or equal to 20", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_text_missing.json b/tests/fixtures/parity/expected/review__422_text_missing.json new file mode 100644 index 0000000..627bead --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_text_missing.json @@ -0,0 +1,31 @@ +{ + "error": { + "cause": "payload.text: Field required", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.text: Field required\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.text: Field required", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.text: Field required", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_text_wrong_type.json b/tests/fixtures/parity/expected/review__422_text_wrong_type.json new file mode 100644 index 0000000..66dd498 --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_text_wrong_type.json @@ -0,0 +1,31 @@ +{ + "error": { + "cause": "payload.text: Input should be a valid string", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Input should be a valid string" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.text: Input should be a valid string\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.text: Input should be a valid string", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.text: Input should be a valid string", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_unknown_escalate_field.json b/tests/fixtures/parity/expected/review__422_unknown_escalate_field.json new file mode 100644 index 0000000..78a92fb --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_unknown_escalate_field.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "payload.escalate.nope: Extra inputs are not permitted", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "nope" + ], + "msg": "Extra inputs are not permitted" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.escalate.nope: Extra inputs are not permitted\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.escalate.nope: Extra inputs are not permitted", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.escalate.nope: Extra inputs are not permitted", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_unknown_top_level_field.json b/tests/fixtures/parity/expected/review__422_unknown_top_level_field.json new file mode 100644 index 0000000..110cec7 --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_unknown_top_level_field.json @@ -0,0 +1,31 @@ +{ + "error": { + "cause": "payload.max_verifications: Extra inputs are not permitted", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "max_verifications" + ], + "msg": "Extra inputs are not permitted" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.max_verifications: Extra inputs are not permitted\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.max_verifications: Extra inputs are not permitted", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.max_verifications: Extra inputs are not permitted", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_unsupported_language.json b/tests/fixtures/parity/expected/review__422_unsupported_language.json new file mode 100644 index 0000000..f15e35e --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_unsupported_language.json @@ -0,0 +1,30 @@ +{ + "error": { + "cause": "language: Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "language" + ], + "msg": "language: Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "language: Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.\n Fix: Check the request body against the OpenAPI spec.", + "message": "language: Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "language: Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__422_visibility_public.json b/tests/fixtures/parity/expected/review__422_visibility_public.json new file mode 100644 index 0000000..a4c768d --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_visibility_public.json @@ -0,0 +1,31 @@ +{ + "error": { + "cause": "payload.visibility: Input should be 'private' or 'unlisted'", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "visibility" + ], + "msg": "Input should be 'private' or 'unlisted'" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "payload.visibility: Input should be 'private' or 'unlisted'\n Fix: Check the request body against the OpenAPI spec.", + "message": "payload.visibility: Input should be 'private' or 'unlisted'", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "payload.visibility: Input should be 'private' or 'unlisted'", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__429_extract_daily_limit.json b/tests/fixtures/parity/expected/review__429_extract_daily_limit.json new file mode 100644 index 0000000..9486b2b --- /dev/null +++ b/tests/fixtures/parity/expected/review__429_extract_daily_limit.json @@ -0,0 +1,25 @@ +{ + "error": { + "cause": "Daily /extract limit of 1000 reached for this account. A review of a URL reads the page like /extract and counts as one /extract call.", + "code": "extract_daily_limit", + "doc_url": "https://lenz.io/docs/rate-limits", + "fix": "Wait Retry-After seconds and retry.", + "friendly_text": "Daily /extract limit of 1000 reached for this account. A review of a URL reads the page like /extract and counts as one /extract call.\n Fix: Wait Retry-After seconds and retry.", + "limit": 1000, + "message": "Daily /extract limit of 1000 reached for this account. A review of a URL reads the page like /extract and counts as one /extract call.", + "payload_json": { + "error": { + "code": "rate_limited", + "fix": "Wait Retry-After seconds and retry.", + "message": "Daily /extract limit of 1000 reached for this account. A review of a URL reads the page like /extract and counts as one /extract call.", + "status": 429 + } + }, + "request_id": "", + "reset_in_seconds": 60, + "retry_after": 60, + "status_code": 429, + "type": "LenzRateLimitError", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/expected/review__429_review_in_flight.json b/tests/fixtures/parity/expected/review__429_review_in_flight.json new file mode 100644 index 0000000..2d14064 --- /dev/null +++ b/tests/fixtures/parity/expected/review__429_review_in_flight.json @@ -0,0 +1,25 @@ +{ + "error": { + "cause": "This account already has 3 reviews running. Retry when one completes.", + "code": "review_in_flight", + "doc_url": "https://lenz.io/docs/rate-limits", + "fix": "Wait Retry-After seconds and retry.", + "friendly_text": "This account already has 3 reviews running. Retry when one completes.\n Fix: Wait Retry-After seconds and retry.", + "limit": null, + "message": "This account already has 3 reviews running. Retry when one completes.", + "payload_json": { + "error": { + "code": "rate_limited", + "fix": "Wait Retry-After seconds and retry.", + "message": "This account already has 3 reviews running. Retry when one completes.", + "status": 429 + } + }, + "request_id": "", + "reset_in_seconds": null, + "retry_after": 60, + "status_code": 429, + "type": "LenzRateLimitError", + "upgrade_url": "" + } +} diff --git a/tests/fixtures/parity/expected/review__503_capacity.json b/tests/fixtures/parity/expected/review__503_capacity.json new file mode 100644 index 0000000..5384727 --- /dev/null +++ b/tests/fixtures/parity/expected/review__503_capacity.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 90, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/review__503_citations_capacity.json b/tests/fixtures/parity/expected/review__503_citations_capacity.json new file mode 100644 index 0000000..5384727 --- /dev/null +++ b/tests/fixtures/parity/expected/review__503_citations_capacity.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "status": 503 + } + }, + "request_id": "", + "retry_after": 90, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/review__get_403_insufficient_scope.json b/tests/fixtures/parity/expected/review__get_403_insufficient_scope.json new file mode 100644 index 0000000..567f3ae --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_403_insufficient_scope.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "The access token does not allow this call.", + "code": "insufficient_scope", + "doc_url": "https://lenz.io/docs/auth", + "fix": "This key doesn't have access to that resource.", + "friendly_text": "The access token does not allow this call.\n Fix: This key doesn't have access to that resource.", + "message": "The access token does not allow this call.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "This key doesn't have access to that resource.", + "message": "The access token does not allow this call.", + "status": 403 + } + }, + "request_id": "", + "status_code": 403, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/review__get_404_not_found.json b/tests/fixtures/parity/expected/review__get_404_not_found.json new file mode 100644 index 0000000..528e617 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_404_not_found.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Review not found.", + "code": "not_found", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Review not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Review not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Review not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/review__get_404_other_account.json b/tests/fixtures/parity/expected/review__get_404_other_account.json new file mode 100644 index 0000000..528e617 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_404_other_account.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Review not found.", + "code": "not_found", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Review not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Review not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Review not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/review__get_410_purged.json b/tests/fixtures/parity/expected/review__get_410_purged.json new file mode 100644 index 0000000..57658a2 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_410_purged.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "This review is no longer available: its account removes content after a set period.", + "code": "purged", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Its account's retention period removed it. A certificate issued for it is still available.", + "friendly_text": "This review is no longer available: its account removes content after a set period.\n Fix: Its account's retention period removed it. A certificate issued for it is still available.", + "message": "This review is no longer available: its account removes content after a set period.", + "payload_json": { + "error": { + "code": "gone", + "fix": "Its account's retention period removed it. A certificate issued for it is still available.", + "message": "This review is no longer available: its account removes content after a set period.", + "status": 410 + } + }, + "purged_at": "2026-10-01T09:07:00.123456Z", + "request_id": "", + "status_code": 410, + "type": "LenzGoneError" + } +} diff --git a/tests/fixtures/parity/expected/review__get_422_unknown_view.json b/tests/fixtures/parity/expected/review__get_422_unknown_view.json new file mode 100644 index 0000000..7d2cc01 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_422_unknown_view.json @@ -0,0 +1,30 @@ +{ + "error": { + "cause": "view: use full or issues.", + "code": "validation_error", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "query", + "view" + ], + "msg": "view: use full or issues." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "view: use full or issues.\n Fix: Check the request body against the OpenAPI spec.", + "message": "view: use full or issues.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "view: use full or issues.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__get_assessing.json b/tests/fixtures/parity/expected/review__get_assessing.json new file mode 100644 index 0000000..72f3ffc --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_assessing.json @@ -0,0 +1,108 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "running", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": null, + "index": 0, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "running", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": null, + "index": 1, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 10, + "review_id": "d6b2bd72", + "status": "assessing", + "summary": { + "assessments": { + "completed": 0, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: assessing — 2 claims: 2 not checked · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n ? · quick check\n\n[2/2] Water boils at 100 C at sea level.\n ? · quick check\n", + "render_issues": "Review d6b2bd72: assessing — 2 claims: 2 not checked · 2 credits charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_assessment_rows_full_fields.json b/tests/fixtures/parity/expected/review__get_assessment_rows_full_fields.json new file mode 100644 index 0000000..8322f9e --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_assessment_rows_full_fields.json @@ -0,0 +1,168 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": "One reviewer read the claim as about the entry into force.", + "error_code": null, + "failure": null, + "hint": "This text holds more than one claim.", + "identified_claims": [ + "The EU AI Act took effect in 2024.", + "It took effect in March." + ], + "rationale": "Two reviewers found the date wrong.", + "status": "completed", + "suggested_rewrite": "The EU AI Act entered into force in August 2024.", + "verdict": "False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "cap", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": "timeout", + "failure": { + "docs_url": null, + "failure_class": "upstream_unavailable", + "failure_reason": "timeout", + "hint": "The check ran out of time. Retry it; nothing was charged.", + "retryable": true + }, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "failed", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": null, + "index": 1, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 1, + "failure": { + "docs_url": null, + "failure_class": "upstream_unavailable", + "failure_reason": "timeout", + "hint": "The check ran out of time. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + } + ], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "cap", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": "Two reviewers found the date wrong.", + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": "The EU AI Act entered into force in August 2024.", + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": null, + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 0, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 1 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: incomplete — 2 claims: 1 issue, 1 failed · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n Two reviewers found the date wrong.\n Not deep-checked (cap).\n\n[2/2] The Eiffel Tower is in Berlin.\n Quick check failed: The check ran out of time. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: incomplete — 2 claims: 1 issue, 1 failed · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n Two reviewers found the date wrong.\n Suggested rewrite: The EU AI Act entered into force in August 2024.\n Not deep-checked (cap).\n\n[2/2] The Eiffel Tower is in Berlin.\n Assessment failed: The check ran out of time. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_clean.json b/tests/fixtures/parity/expected/review__get_citations_clean.json new file mode 100644 index 0000000..b9ecbce --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_clean.json @@ -0,0 +1,181 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 checked.\n", + "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_failed_and_unchecked.json b/tests/fixtures/parity/expected/review__get_citations_failed_and_unchecked.json new file mode 100644 index 0000000..81f36b2 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_failed_and_unchecked.json @@ -0,0 +1,197 @@ +{ + "dump": { + "citation_failures": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#internal", + "failure_class": "internal", + "failure_reason": "internal", + "hint": "The check failed on our side. Try again later.", + "retryable": false + }, + "reference": "a report from the ministry" + } + ], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#internal", + "failure_class": "internal", + "failure_reason": "internal", + "hint": "The check failed on our side. Try again later.", + "retryable": false + }, + "hint": "The check failed on our side. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": null, + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "unchecked", + "unchecked_reason": "no_text" + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "unchecked", + "is_issue": false, + "source": null + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 0, + "failed": 1, + "unchecked": 1 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: incomplete — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 could not be checked.\n\n[source 1/2] Could not be checked this time.\n https://example.gov/report-2024\n The check failed on our side. Try again later.\n\nCheck these by hand (Lenz could not check them)\n[source 2/2] The page gave no text to read.\n https://example.org/statement\n", + "render_issues": "Review d6b2bd72: incomplete — 1 claim: 1 true or mostly true · 1 credit charged\n\n2 sources cited in your draft\n2 could not be checked.\n\n[source 1/2] Could not be checked this time.\n https://example.gov/report-2024\n The check failed on our side. Try again later.\n\nCheck these by hand (Lenz could not check them)\n[source 2/2] The page gave no text to read.\n https://example.org/statement\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_issue_full.json b/tests/fixtures/parity/expected/review__get_citations_issue_full.json new file mode 100644 index 0000000..cbb418a --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_issue_full.json @@ -0,0 +1,203 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": null, + "finding": "contradicted", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "rationale": "The page states it.", + "reference": "a report from the ministry", + "snippet": "The source says so.", + "source": "support", + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "contradicted", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "contradicted", + "is_issue": true, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 1, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 1, + "render": "Review d6b2bd72: issues found — 1 claim: 1 true or mostly true · 3 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", + "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 true or mostly true · 3 credits charged\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_issue_issues_view.json b/tests/fixtures/parity/expected/review__get_citations_issue_issues_view.json new file mode 100644 index 0000000..59abdb7 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_issue_issues_view.json @@ -0,0 +1,93 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": null, + "finding": "contradicted", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "rationale": "The page states it.", + "reference": "a report from the ministry", + "snippet": "The source says so.", + "source": "support", + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 1, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "issues" + }, + "exit_code": 1, + "render": "Review d6b2bd72: issues found — 1 claim: 1 not checked · 3 credits charged\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", + "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 not checked · 3 credits charged\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_limit_exactly_at_limit.json b/tests/fixtures/parity/expected/review__get_citations_limit_exactly_at_limit.json new file mode 100644 index 0000000..21955f5 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_limit_exactly_at_limit.json @@ -0,0 +1,181 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 2, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 checked.\n", + "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_limit_reached.json b/tests/fixtures/parity/expected/review__get_citations_limit_reached.json new file mode 100644 index 0000000..5e3bb88 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_limit_reached.json @@ -0,0 +1,155 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [ + { + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "reference": "a statement", + "sentence": "The agency said so in a statement." + } + ], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 1, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 1, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 1, + "citation_limit_reached": true, + "citations_found": 2, + "citations_selected": 1, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 2 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n1 checked.\n1 more was not checked: one check covers 1.\n\n1 more citation was found but not checked.\n", + "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 2 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n1 checked.\n1 more was not checked: one check covers 1.\n\n1 more citation was found but not checked.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_no_claim_but_citations.json b/tests/fixtures/parity/expected/review__get_citations_no_claim_but_citations.json new file mode 100644 index 0000000..8093baf --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_no_claim_but_citations.json @@ -0,0 +1,150 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 0, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 0, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — no claims checked · 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n", + "render_issues": "Review d6b2bd72: clean — no claims checked · 2 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_no_claim_no_citation.json b/tests/fixtures/parity/expected/review__get_citations_no_claim_no_citation.json new file mode 100644 index 0000000..c088cac --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_no_claim_no_citation.json @@ -0,0 +1,66 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_claim", + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "retryable": false + }, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": "unchecked", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run\nthe text through /extract to enumerate its claims.\n", + "render_issues": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run\nthe text through /extract to enumerate its claims.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_not_asked.json b/tests/fixtures/parity/expected/review__get_citations_not_asked.json new file mode 100644 index 0000000..a7c1d4a --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_not_asked.json @@ -0,0 +1,98 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_only.json b/tests/fixtures/parity/expected/review__get_citations_only.json new file mode 100644 index 0000000..ec2968f --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_only.json @@ -0,0 +1,157 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "positions": null + } + ], + "more_claims": [ + "The EU AI Act took effect in March 2024." + ], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 0, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 0, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 0, + "claim_limit_reached": true, + "claims_selected": 0, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — no claims checked · 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n\n1 more claim was found but not checked.\n", + "render_issues": "Review d6b2bd72: clean — no claims checked · 2 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n\n1 more claim was found but not checked.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_partly_supported.json b/tests/fixtures/parity/expected/review__get_citations_partly_supported.json new file mode 100644 index 0000000..1f7c6fc --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_partly_supported.json @@ -0,0 +1,181 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "partly_supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "partly_supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 checked.\n", + "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_running.json b/tests/fixtures/parity/expected/review__get_citations_running.json new file mode 100644 index 0000000..197c7ad --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_running.json @@ -0,0 +1,165 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "a report from the ministry", + "result": null, + "statement": null + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "a statement", + "result": null, + "statement": null + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 15, + "review_id": "d6b2bd72", + "status": "verifying", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 0, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: verifying — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\nLenz checks each one: does the source say what your draft says it does.\n", + "render_issues": "Review d6b2bd72: verifying — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n\n2 sources cited in your draft\nLenz checks each one: does the source say what your draft says it does.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_skipped_switched_off.json b/tests/fixtures/parity/expected/review__get_citations_skipped_switched_off.json new file mode 100644 index 0000000..9656e4e --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_skipped_switched_off.json @@ -0,0 +1,98 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": "switched_off", + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_citations_skipped_url_input.json b/tests/fixtures/parity/expected/review__get_citations_skipped_url_input.json new file mode 100644 index 0000000..4a4d436 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_citations_skipped_url_input.json @@ -0,0 +1,66 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": null, + "retryable": true + }, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": "unchecked", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": "url_input", + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: upstream_unavailable\n\nSources on a linked page are not checked. Paste the page's text instead to check them.\n", + "render_issues": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: upstream_unavailable\n\nSources on a linked page are not checked. Paste the page's text instead to check them.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_claim_limit_not_reached.json b/tests/fixtures/parity/expected/review__get_claim_limit_not_reached.json new file mode 100644 index 0000000..fe88fdc --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_claim_limit_not_reached.json @@ -0,0 +1,128 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 3, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 3, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_claim_limit_reached_exact.json b/tests/fixtures/parity/expected/review__get_claim_limit_reached_exact.json new file mode 100644 index 0000000..50f547c --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_claim_limit_reached_exact.json @@ -0,0 +1,128 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 2, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 2, + "claim_limit_reached": true, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_completed_assess_only.json b/tests/fixtures/parity/expected/review__get_completed_assess_only.json new file mode 100644 index 0000000..382a665 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_completed_assess_only.json @@ -0,0 +1,154 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "cap", + "matched_rules": [ + "verdict" + ] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 1, + "confidence": "high", + "escalation": { + "disposition": "cap", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": null, + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 0, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 1, + "render": "Review d6b2bd72: issues found — 2 claims: 1 issue, 1 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n[2/2] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Not deep-checked (cap).\n", + "render_issues": "Review d6b2bd72: issues found — 2 claims: 1 issue, 1 true or mostly true · 2 credits charged\n\n[2/2] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Not deep-checked (cap).\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_completed_clean.json b/tests/fixtures/parity/expected/review__get_completed_clean.json new file mode 100644 index 0000000..b028662 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_completed_clean.json @@ -0,0 +1,128 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Paris is the capital of France.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n[2/2] Paris is the capital of France.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_completed_issues_all_verified.json b/tests/fixtures/parity/expected/review__get_completed_issues_all_verified.json new file mode 100644 index 0000000..aa86f83 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_completed_issues_all_verified.json @@ -0,0 +1,259 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "medium", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "Mostly False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The EU AI Act took effect in March 2024.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T09:49:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The EU AI Act took effect in March 2024.", + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "task_id": "c010dca9a43543f518e49f4ec7842ceb", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verdict": "False", + "verification_id": "b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "visibility": "private", + "warnings": [ + "w1" + ] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T09:49:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "task_id": "4634b89821fd877f1edf8aaeb82b7ee5", + "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "verdict": "False", + "verification_id": "b79019e4", + "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", + "visibility": "private", + "warnings": [ + "w1" + ] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Paris is the capital of France.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 2, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 23 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verdict": "False", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verified_claim": null + }, + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 1, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "verdict": "False", + "verification_id": "b79019e4", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 3, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 3, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 2, + "failed": 0, + "planned": 2 + } + }, + "view": "full" + }, + "exit_code": 1, + "render": "Review d6b2bd72: issues found — 3 claims: 2 issues, 1 true or mostly true · 2 of 2 deep-checked · 23 credits charged\n\n[1/3] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[2/3] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b79019e4\n\n[3/3] Paris is the capital of France.\n True (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: issues found — 3 claims: 2 issues, 1 true or mostly true · 2 of 2 deep-checked · 23 credits charged\n\n[1/3] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[2/3] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b79019e4\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_completed_issues_full.json b/tests/fixtures/parity/expected/review__get_completed_issues_full.json new file mode 100644 index 0000000..b2bfc03 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_completed_issues_full.json @@ -0,0 +1,310 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "medium", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "Mostly False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The EU AI Act took effect in March 2024.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T09:42:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The EU AI Act took effect in March 2024.", + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "task_id": "7df4009bc27aab64ce5d5a01fbafe599", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verdict": "False", + "verification_id": "b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "visibility": "private", + "warnings": [ + "w1" + ] + } + }, + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": "upstream_unavailable", + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "failed", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": null, + "index": 1, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 2, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": null, + "confidence": null, + "content_status": "available", + "created_at": null, + "depth": null, + "domain": null, + "entities": [], + "executive_summary": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "hint": null, + "retryable": false + }, + "key_finding": null, + "language": null, + "lenz_score": null, + "modified_at": null, + "status": "failed", + "suggested_rewrite": null, + "task_id": "5ccac77f53ff49328ec134fb29d66542", + "url": null, + "verdict": null, + "verification_id": null, + "verification_url": null, + "visibility": null, + "warnings": [] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Paris is the capital of France.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 3, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 13 + }, + "failure": null, + "failures": [ + { + "claim": "Water boils at 100 C at sea level.", + "claim_index": 1, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + } + ], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verdict": "False", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verified_claim": null + }, + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 2, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "hint": null, + "retryable": false + }, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 3, + "failed": 1 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 4, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 1, + "failed": 1, + "planned": 2 + } + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 true or mostly true, 1 failed · 1 of 2 deep-checked · 13 credits \ncharged\n\n[1/4] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[2/4] Water boils at 100 C at sea level.\n Quick check failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\n[3/4] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Deep check failed: The deep check failed.\n\n[4/4] Paris is the capital of France.\n True (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 true or mostly true, 1 failed · 1 of 2 deep-checked · 13 credits \ncharged\n\n[1/4] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[3/4] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Deep check failed: research_empty\n\n[2/4] Water boils at 100 C at sea level.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_completed_issues_view_issues.json b/tests/fixtures/parity/expected/review__get_completed_issues_view_issues.json new file mode 100644 index 0000000..c0c2c56 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_completed_issues_view_issues.json @@ -0,0 +1,133 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 13 + }, + "failure": null, + "failures": [ + { + "claim": "Water boils at 100 C at sea level.", + "claim_index": 1, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + } + ], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verdict": "False", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verified_claim": null + }, + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 2, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "hint": null, + "retryable": false + }, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 3, + "failed": 1 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 4, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 1, + "failed": 1, + "planned": 2 + } + }, + "view": "issues" + }, + "exit_code": 2, + "render": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 failed, 1 not checked · 1 of 2 deep-checked · 13 credits charged\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 failed, 1 not checked · 1 of 2 deep-checked · 13 credits charged\n\n[1/4] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[3/4] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Deep check failed: research_empty\n\n[2/4] Water boils at 100 C at sea level.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_completed_positions.json b/tests/fixtures/parity/expected/review__get_completed_positions.json new file mode 100644 index 0000000..f89f5be --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_completed_positions.json @@ -0,0 +1,140 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [ + { + "end": 40, + "start": 0, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": [ + { + "end": 89, + "start": 55, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024. · at 0-40\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level. · at 55-89\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_completed_recovered_claim.json b/tests/fixtures/parity/expected/review__get_completed_recovered_claim.json new file mode 100644 index 0000000..dde95e1 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_completed_recovered_claim.json @@ -0,0 +1,98 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Orbit Motors stock increased by 40% in 2022.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] Orbit Motors stock increased by 40% in 2022.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_completed_with_untraced_claim.json b/tests/fixtures/parity/expected/review__get_completed_with_untraced_claim.json new file mode 100644 index 0000000..cdb1fd6 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_completed_with_untraced_claim.json @@ -0,0 +1,140 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [ + { + "end": 40, + "start": 0, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": [ + { + "end": 84, + "start": 50, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024. · at 0-40\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level. · at 50-84\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_during_quick_check.json b/tests/fixtures/parity/expected/review__get_during_quick_check.json new file mode 100644 index 0000000..efe850f --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_during_quick_check.json @@ -0,0 +1,86 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "running", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": null, + "index": 0, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 10, + "review_id": "d6b2bd72", + "status": "assessing", + "summary": { + "assessments": { + "completed": 0, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: assessing — 1 claim: 1 not checked · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n ? · quick check\n", + "render_issues": "Review d6b2bd72: assessing — 1 claim: 1 not checked · 1 credit charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_failed_every_assessment_failed.json b/tests/fixtures/parity/expected/review__get_failed_every_assessment_failed.json new file mode 100644 index 0000000..d9d5414 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_failed_every_assessment_failed.json @@ -0,0 +1,155 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": "upstream_unavailable", + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "failed", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": null, + "index": 0, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": "upstream_unavailable", + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "failed", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": null, + "index": 1, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "assessment_failed", + "hint": "No claim could be assessed. Retry the review with a new Idempotency-Key; nothing was charged.", + "retryable": true + }, + "failures": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + }, + { + "claim": "Water boils at 100 C at sea level.", + "claim_index": 1, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + } + ], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": { + "completed": 0, + "failed": 2 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: incomplete — 2 claims: 2 failed · 0 credits charged\nFailed: No claim could be assessed. Retry the review with a new Idempotency-Key; nothing was charged.\n\n[1/2] The EU AI Act took effect in March 2024.\n Quick check failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\n[2/2] Water boils at 100 C at sea level.\n Quick check failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n", + "render_issues": "Review d6b2bd72: incomplete — 2 claims: 2 failed · 0 credits charged\nFailed: No claim could be assessed. Retry the review with a new Idempotency-Key; nothing was charged.\n\n[1/2] The EU AI Act took effect in March 2024.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\n[2/2] Water boils at 100 C at sea level.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_failed_no_claim.json b/tests/fixtures/parity/expected/review__get_failed_no_claim.json new file mode 100644 index 0000000..8a2a3a1 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_failed_no_claim.json @@ -0,0 +1,66 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_claim", + "hint": "The text is a greeting, not a statement about the world. Send one factual claim, or run the text through /extract to enumerate its claims.", + "retryable": false + }, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": "unchecked", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: The text is a greeting, not a statement about the world. Send one factual claim, or run the text through \n/extract to enumerate its claims.\n", + "render_issues": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: The text is a greeting, not a statement about the world. Send one factual claim, or run the text through \n/extract to enumerate its claims.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_failed_no_claim_untraced.json b/tests/fixtures/parity/expected/review__get_failed_no_claim_untraced.json new file mode 100644 index 0000000..435f4a6 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_failed_no_claim_untraced.json @@ -0,0 +1,66 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_claim", + "hint": "None of the claims found in the text could be traced back to it. Send one factual claim, or run the text through /extract to enumerate its claims.", + "retryable": false + }, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": "unchecked", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: None of the claims found in the text could be traced back to it. Send one factual claim, or run the text through\n/extract to enumerate its claims.\n", + "render_issues": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: None of the claims found in the text could be traced back to it. Send one factual claim, or run the text through\n/extract to enumerate its claims.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_more_claims.json b/tests/fixtures/parity/expected/review__get_more_claims.json new file mode 100644 index 0000000..e22d03e --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_more_claims.json @@ -0,0 +1,153 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [ + { + "end": 40, + "start": 0, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": [ + { + "end": 84, + "start": 50, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "positions": [ + { + "end": 121, + "start": 91, + "text": "The Eiffel Tower is in Berlin." + } + ] + } + ], + "more_claims": [ + "The Eiffel Tower is in Berlin." + ], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 2, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 2, + "claim_limit_reached": true, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024. · at 0-40\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level. · at 50-84\n True (high) · quick check\n\n1 more claim was found but not checked.\n", + "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n\n1 more claim was found but not checked.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..56e09af --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,149 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "language": "en", + "lenz_score": 2, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "status": "completed", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "task_id": "7df4009bc27aab64ce5d5a01fbafe599", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verdict": "False", + "verification_id": "b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "visibility": "private", + "warnings": [ + "w1" + ] + } + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 11 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verdict": "False", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 1, + "failed": 0, + "planned": 1 + } + }, + "view": "full" + }, + "exit_code": 1, + "render": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..ac41e67 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_same_day_hours_apart.json @@ -0,0 +1,149 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "task_id": "7df4009bc27aab64ce5d5a01fbafe599", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verdict": "False", + "verification_id": "b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "visibility": "private", + "warnings": [ + "w1" + ] + } + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 11 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verdict": "False", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 1, + "failed": 0, + "planned": 1 + } + }, + "view": "full" + }, + "exit_code": 1, + "render": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_policy_resolved.json b/tests/fixtures/parity/expected/review__get_policy_resolved.json new file mode 100644 index 0000000..d49a7f7 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_policy_resolved.json @@ -0,0 +1,96 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "low", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 2, + "suggest_edits": false, + "verdicts": [ + "False" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_queued.json b/tests/fixtures/parity/expected/review__get_queued.json new file mode 100644 index 0000000..00606b0 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_queued.json @@ -0,0 +1,60 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 10, + "review_id": "d6b2bd72", + "status": "queued", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: queued — no claims checked · 0 credits charged\n", + "render_issues": "Review d6b2bd72: queued — no claims checked · 0 credits charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_suggested_edits.json b/tests/fixtures/parity/expected/review__get_suggested_edits.json new file mode 100644 index 0000000..9624e1d --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_suggested_edits.json @@ -0,0 +1,267 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": [ + { + "end": 30, + "start": 0, + "text": "The Eiffel Tower is in Berlin." + } + ], + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": { + "edits": [ + { + "end": 29, + "position": 0, + "replacement": "Paris", + "start": 23, + "text": "Berlin" + } + ], + "status": "completed" + }, + "verification": { + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T09:35:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "task_id": "9bca1e0819de60a750186e812dc58e2b", + "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "verdict": "False", + "verification_id": "7c83247c", + "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "visibility": "private", + "warnings": [ + "w1" + ] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 1, + "positions": [ + { + "end": 85, + "start": 45, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": { + "edits": null, + "status": "pending" + }, + "verification": { + "claim": null, + "confidence": null, + "content_status": "available", + "created_at": null, + "depth": null, + "domain": null, + "entities": [], + "executive_summary": null, + "failure": null, + "key_finding": null, + "language": null, + "lenz_score": null, + "modified_at": null, + "status": "processing", + "suggested_rewrite": null, + "task_id": null, + "url": null, + "verdict": null, + "verification_id": null, + "verification_url": null, + "visibility": null, + "warnings": [] + } + } + ], + "completed_at": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 12 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": { + "edits": [ + { + "end": 29, + "position": 0, + "replacement": "Paris", + "start": 23, + "text": "Berlin" + } + ], + "status": "completed" + }, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "verdict": "False", + "verification_id": "7c83247c", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "verified_claim": null + }, + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 1, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": { + "edits": null, + "status": "pending" + }, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": true, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 15, + "review_id": "d6b2bd72", + "status": "verifying", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 1, + "failed": 0, + "planned": 2 + } + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\n[1/2] The Eiffel Tower is in Berlin. · at 0-30\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-7c83247c\n\n[2/2] The EU AI Act took effect in March 2024. · at 45-85\n False (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\n[1/2] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-7c83247c\n\n[2/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_suggested_edits_issues_view.json b/tests/fixtures/parity/expected/review__get_suggested_edits_issues_view.json new file mode 100644 index 0000000..2f8589c --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_suggested_edits_issues_view.json @@ -0,0 +1,128 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 12 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": { + "edits": [ + { + "end": 29, + "position": 0, + "replacement": "Paris", + "start": 23, + "text": "Berlin" + } + ], + "status": "completed" + }, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "verdict": "False", + "verification_id": "7c83247c", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "verified_claim": null + }, + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 1, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": { + "edits": null, + "status": "pending" + }, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": true, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 15, + "review_id": "d6b2bd72", + "status": "verifying", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 1, + "failed": 0, + "planned": 2 + } + }, + "view": "issues" + }, + "exit_code": 2, + "render": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\n[1/2] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-7c83247c\n\n[2/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_url_review.json b/tests/fixtures/parity/expected/review__get_url_review.json new file mode 100644 index 0000000..ea99aa8 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_url_review.json @@ -0,0 +1,104 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [ + { + "end": null, + "start": null, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "exit_code": 0, + "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__get_verifying.json b/tests/fixtures/parity/expected/review__get_verifying.json new file mode 100644 index 0000000..0b901c0 --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_verifying.json @@ -0,0 +1,177 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": null, + "confidence": null, + "content_status": "available", + "created_at": null, + "depth": null, + "domain": null, + "entities": [], + "executive_summary": null, + "failure": null, + "key_finding": null, + "language": null, + "lenz_score": null, + "modified_at": null, + "status": "processing", + "suggested_rewrite": null, + "task_id": null, + "url": null, + "verdict": null, + "verification_id": null, + "verification_url": null, + "visibility": null, + "warnings": [] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 15, + "review_id": "d6b2bd72", + "status": "verifying", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 1 + } + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: verifying — 2 claims: 1 issue, 1 true or mostly true · 0 of 1 deep-checked · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n\n[2/2] Water boils at 100 C at sea level.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: verifying — 2 claims: 1 issue, 1 true or mostly true · 0 of 1 deep-checked · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n", + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__idempotency_body_mismatch_422.json b/tests/fixtures/parity/expected/review__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..5de20e3 --- /dev/null +++ b/tests/fixtures/parity/expected/review__idempotency_body_mismatch_422.json @@ -0,0 +1,29 @@ +{ + "error": { + "cause": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "header" + ], + "msg": "Idempotency-Key reused with a different request body." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Idempotency-Key reused with a different request body.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__idempotency_conflict_409.json b/tests/fixtures/parity/expected/review__idempotency_conflict_409.json new file mode 100644 index 0000000..6f3c206 --- /dev/null +++ b/tests/fixtures/parity/expected/review__idempotency_conflict_409.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "A review with this Idempotency-Key is still being created. Retry shortly.", + "code": "idempotency_conflict", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "A review with this Idempotency-Key is still being created. Retry shortly.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "A review with this Idempotency-Key is still being created. Retry shortly.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "A review with this Idempotency-Key is still being created. Retry shortly.", + "status": 409 + } + }, + "request_id": "", + "status_code": 409, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/review__idempotency_conflict_409_existing_review.json b/tests/fixtures/parity/expected/review__idempotency_conflict_409_existing_review.json new file mode 100644 index 0000000..6f3c206 --- /dev/null +++ b/tests/fixtures/parity/expected/review__idempotency_conflict_409_existing_review.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "A review with this Idempotency-Key is still being created. Retry shortly.", + "code": "idempotency_conflict", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "A review with this Idempotency-Key is still being created. Retry shortly.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "A review with this Idempotency-Key is still being created. Retry shortly.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "A review with this Idempotency-Key is still being created. Retry shortly.", + "status": 409 + } + }, + "request_id": "", + "status_code": 409, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/review__idempotent_replay_202.json b/tests/fixtures/parity/expected/review__idempotent_replay_202.json new file mode 100644 index 0000000..17e07a5 --- /dev/null +++ b/tests/fixtures/parity/expected/review__idempotent_replay_202.json @@ -0,0 +1,7 @@ +{ + "dump": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__receipt_202.json b/tests/fixtures/parity/expected/review__receipt_202.json new file mode 100644 index 0000000..17e07a5 --- /dev/null +++ b/tests/fixtures/parity/expected/review__receipt_202.json @@ -0,0 +1,7 @@ +{ + "dump": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/expected/review__receipt_202_empty_webhook_url.json new file mode 100644 index 0000000..17e07a5 --- /dev/null +++ b/tests/fixtures/parity/expected/review__receipt_202_empty_webhook_url.json @@ -0,0 +1,7 @@ +{ + "dump": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__receipt_202_webhook_url.json b/tests/fixtures/parity/expected/review__receipt_202_webhook_url.json new file mode 100644 index 0000000..17e07a5 --- /dev/null +++ b/tests/fixtures/parity/expected/review__receipt_202_webhook_url.json @@ -0,0 +1,7 @@ +{ + "dump": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/review__replay_later_202.json b/tests/fixtures/parity/expected/review__replay_later_202.json new file mode 100644 index 0000000..17e07a5 --- /dev/null +++ b/tests/fixtures/parity/expected/review__replay_later_202.json @@ -0,0 +1,7 @@ +{ + "dump": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__batch_202.json b/tests/fixtures/parity/expected/verify__batch_202.json new file mode 100644 index 0000000..d044b7c --- /dev/null +++ b/tests/fixtures/parity/expected/verify__batch_202.json @@ -0,0 +1,16 @@ +{ + "dump": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "claim_text": "The Earth is round.", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be" + }, + { + "claim_text": "Water boils at 100C at sea level.", + "task_id": "13610f0b64b63e006dc66364e0e82c85" + } + ] + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__batch_capacity_503.json b/tests/fixtures/parity/expected/verify__batch_capacity_503.json new file mode 100644 index 0000000..11cc917 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__batch_capacity_503.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "status": 503 + } + }, + "request_id": "", + "retry_after": null, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/verify__batch_empty_422.json b/tests/fixtures/parity/expected/verify__batch_empty_422.json new file mode 100644 index 0000000..e1ddac5 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__batch_empty_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "claims is required and must be non-empty.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "claims is required and must be non-empty.\n Fix: Check the request body against the OpenAPI spec.", + "message": "claims is required and must be non-empty.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "claims is required and must be non-empty.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__batch_idempotency_conflict_409.json b/tests/fixtures/parity/expected/verify__batch_idempotency_conflict_409.json new file mode 100644 index 0000000..54ca3d5 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__batch_idempotency_conflict_409.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "A request with this Idempotency-Key is already in progress.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "A request with this Idempotency-Key is already in progress.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "A request with this Idempotency-Key is already in progress.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "A request with this Idempotency-Key is already in progress.", + "status": 409 + } + }, + "request_id": "", + "status_code": 409, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__batch_item_missing_claim_422.json b/tests/fixtures/parity/expected/verify__batch_item_missing_claim_422.json new file mode 100644 index 0000000..f2a3fe8 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__batch_item_missing_claim_422.json @@ -0,0 +1,34 @@ +{ + "error": { + "cause": "[{'type': 'missing', 'loc': ['body', 'payload', 'claims', 1, 'text'], 'msg': 'Field required'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "claims", + 1, + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'missing', 'loc': ['body', 'payload', 'claims', 1, 'text'], 'msg': 'Field required'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'claims', 1, 'text'], 'msg': 'Field required'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'claims', 1, 'text'], 'msg': 'Field required'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__batch_no_credits_402.json b/tests/fixtures/parity/expected/verify__batch_no_credits_402.json new file mode 100644 index 0000000..806620a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__batch_no_credits_402.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "Insufficient credits for batch.", + "code": "no_credits", + "cost": 10, + "credit_balance": 0, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "Insufficient credits for batch.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "Insufficient credits for batch.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "Insufficient credits for batch.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 0, + "request_id": "", + "requested": 1, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/verify__batch_partial_202.json b/tests/fixtures/parity/expected/verify__batch_partial_202.json new file mode 100644 index 0000000..96fb74b --- /dev/null +++ b/tests/fixtures/parity/expected/verify__batch_partial_202.json @@ -0,0 +1,13 @@ +{ + "dump": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "claim_text": "First claim.", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be" + } + ], + "partial": true + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__batch_too_many_422.json b/tests/fixtures/parity/expected/verify__batch_too_many_422.json new file mode 100644 index 0000000..c07dafe --- /dev/null +++ b/tests/fixtures/parity/expected/verify__batch_too_many_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Batch size exceeds maximum of 20.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Batch size exceeds maximum of 20.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Batch size exceeds maximum of 20.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Batch size exceeds maximum of 20.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__blank_claim_422.json b/tests/fixtures/parity/expected/verify__blank_claim_422.json new file mode 100644 index 0000000..872ff11 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__blank_claim_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Text is required.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Text is required.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Text is required.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Text is required.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__capacity_503.json b/tests/fixtures/parity/expected/verify__capacity_503.json new file mode 100644 index 0000000..11cc917 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__capacity_503.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "doc_url": "https://lenz.io/docs/errors#unavailable", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "status": 503 + } + }, + "request_id": "", + "retry_after": null, + "status_code": 503, + "type": "LenzUpstreamUnavailableError" + } +} diff --git a/tests/fixtures/parity/expected/verify__idempotency_body_mismatch_422.json b/tests/fixtures/parity/expected/verify__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..989af48 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__idempotency_body_mismatch_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Idempotency-Key reused with a different request body.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Idempotency-Key reused with a different request body.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Idempotency-Key reused with a different request body.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__idempotency_conflict_409.json b/tests/fixtures/parity/expected/verify__idempotency_conflict_409.json new file mode 100644 index 0000000..54ca3d5 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__idempotency_conflict_409.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "A request with this Idempotency-Key is already in progress.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "A request with this Idempotency-Key is already in progress.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "A request with this Idempotency-Key is already in progress.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "A request with this Idempotency-Key is already in progress.", + "status": 409 + } + }, + "request_id": "", + "status_code": 409, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__idempotency_key_replay.json b/tests/fixtures/parity/expected/verify__idempotency_key_replay.json new file mode 100644 index 0000000..4aa7f3a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__idempotency_key_replay.json @@ -0,0 +1,9 @@ +{ + "dump": { + "chain_id": "78ac4e9f234ea3bd", + "claim_text": "", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__implicit_dedup_200.json b/tests/fixtures/parity/expected/verify__implicit_dedup_200.json new file mode 100644 index 0000000..77b6440 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__implicit_dedup_200.json @@ -0,0 +1,8 @@ +{ + "dump": { + "claim_text": "", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__implicit_repeat_replay.json b/tests/fixtures/parity/expected/verify__implicit_repeat_replay.json new file mode 100644 index 0000000..4aa7f3a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__implicit_repeat_replay.json @@ -0,0 +1,9 @@ +{ + "dump": { + "chain_id": "78ac4e9f234ea3bd", + "claim_text": "", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__invalid_depth_422.json b/tests/fixtures/parity/expected/verify__invalid_depth_422.json new file mode 100644 index 0000000..128dad2 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__invalid_depth_422.json @@ -0,0 +1,35 @@ +{ + "error": { + "cause": "[{'type': 'literal_error', 'loc': ['body', 'payload', 'depth'], 'msg': \"Input should be 'standard' or 'low'\", 'ctx': {'expected': \"'standard' or 'low'\"}}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "ctx": { + "expected": "'standard' or 'low'" + }, + "loc": [ + "body", + "payload", + "depth" + ], + "msg": "Input should be 'standard' or 'low'", + "type": "literal_error" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'literal_error', 'loc': ['body', 'payload', 'depth'], 'msg': \"Input should be 'standard' or 'low'\", 'ctx': {'expected': \"'standard' or 'low'\"}}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'literal_error', 'loc': ['body', 'payload', 'depth'], 'msg': \"Input should be 'standard' or 'low'\", 'ctx': {'expected': \"'standard' or 'low'\"}}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'literal_error', 'loc': ['body', 'payload', 'depth'], 'msg': \"Input should be 'standard' or 'low'\", 'ctx': {'expected': \"'standard' or 'low'\"}}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__invalid_language_422.json b/tests/fixtures/parity/expected/verify__invalid_language_422.json new file mode 100644 index 0000000..b5218f2 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__invalid_language_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__list_200.json b/tests/fixtures/parity/expected/verify__list_200.json new file mode 100644 index 0000000..9b7f3a5 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__list_200.json @@ -0,0 +1,40 @@ +{ + "dump": { + "items": [ + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + }, + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T09:28:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "7c83247c" + } + ], + "page": 1, + "page_size": 20, + "total": 2 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__list_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/verify__list_200_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..8550130 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__list_200_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,25 @@ +{ + "dump": { + "items": [ + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T23:55:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 1, + "page_size": 20, + "total": 1 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__list_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/verify__list_200_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..b333318 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__list_200_modified_at_same_day_hours_apart.json @@ -0,0 +1,25 @@ +{ + "dump": { + "items": [ + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T08:00:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 1, + "page_size": 20, + "total": 1 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__list_clamped_200.json b/tests/fixtures/parity/expected/verify__list_clamped_200.json new file mode 100644 index 0000000..b48e6be --- /dev/null +++ b/tests/fixtures/parity/expected/verify__list_clamped_200.json @@ -0,0 +1,9 @@ +{ + "dump": { + "items": [], + "page": 1, + "page_size": 100, + "total": 0 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__list_empty_200.json b/tests/fixtures/parity/expected/verify__list_empty_200.json new file mode 100644 index 0000000..d628806 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__list_empty_200.json @@ -0,0 +1,9 @@ +{ + "dump": { + "items": [], + "page": 1, + "page_size": 20, + "total": 0 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__list_page2_200.json b/tests/fixtures/parity/expected/verify__list_page2_200.json new file mode 100644 index 0000000..c5aab9a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__list_page2_200.json @@ -0,0 +1,25 @@ +{ + "dump": { + "items": [ + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 2, + "page_size": 2, + "total": 3 + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__misnamed_field_422.json b/tests/fixtures/parity/expected/verify__misnamed_field_422.json new file mode 100644 index 0000000..6760d1a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__misnamed_field_422.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__missing_claim_422.json b/tests/fixtures/parity/expected/verify__missing_claim_422.json new file mode 100644 index 0000000..6760d1a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__missing_claim_422.json @@ -0,0 +1,32 @@ +{ + "error": { + "cause": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required", + "type": "missing" + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]\n Fix: Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "[{'type': 'missing', 'loc': ['body', 'payload', 'text'], 'msg': 'Field required'}]", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__no_credits_402.json b/tests/fixtures/parity/expected/verify__no_credits_402.json new file mode 100644 index 0000000..f3a12a3 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__no_credits_402.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "No remaining claim checks.", + "code": "no_credits", + "cost": 10, + "credit_balance": 0, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "No remaining claim checks.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "No remaining claim checks.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "No remaining claim checks.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 0, + "request_id": "", + "requested": null, + "resets_at": "2026-10-01T09:14:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/verify__replay_later_202.json b/tests/fixtures/parity/expected/verify__replay_later_202.json new file mode 100644 index 0000000..4aa7f3a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__replay_later_202.json @@ -0,0 +1,9 @@ +{ + "dump": { + "chain_id": "78ac4e9f234ea3bd", + "claim_text": "", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__select_202.json b/tests/fixtures/parity/expected/verify__select_202.json new file mode 100644 index 0000000..d044b7c --- /dev/null +++ b/tests/fixtures/parity/expected/verify__select_202.json @@ -0,0 +1,16 @@ +{ + "dump": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "claim_text": "The Earth is round.", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be" + }, + { + "claim_text": "Water boils at 100C at sea level.", + "task_id": "13610f0b64b63e006dc66364e0e82c85" + } + ] + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__select_empty_422.json b/tests/fixtures/parity/expected/verify__select_empty_422.json new file mode 100644 index 0000000..975cb65 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__select_empty_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "texts is required and must be non-empty.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "texts is required and must be non-empty.\n Fix: Check the request body against the OpenAPI spec.", + "message": "texts is required and must be non-empty.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "texts is required and must be non-empty.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__select_invalid_selection_422.json b/tests/fixtures/parity/expected/verify__select_invalid_selection_422.json new file mode 100644 index 0000000..1a020a2 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__select_invalid_selection_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Selected text was not one of the offered claims: 'The Moon is made of cheese.'", + "code": "invalid_selection", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Selected text was not one of the offered claims: 'The Moon is made of cheese.'\n Fix: Check the request body against the OpenAPI spec.", + "message": "Selected text was not one of the offered claims: 'The Moon is made of cheese.'", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Selected text was not one of the offered claims: 'The Moon is made of cheese.'", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__select_no_selection_pending_409.json b/tests/fixtures/parity/expected/verify__select_no_selection_pending_409.json new file mode 100644 index 0000000..fd4d147 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__select_no_selection_pending_409.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "This task has no pending claim selection.", + "code": "no_selection_pending", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "This task has no pending claim selection.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "This task has no pending claim selection.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "This task has no pending claim selection.", + "status": 409 + } + }, + "request_id": "", + "status_code": 409, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__select_too_many_422.json b/tests/fixtures/parity/expected/verify__select_too_many_422.json new file mode 100644 index 0000000..9a93a10 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__select_too_many_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "Cannot select more than 20 claims.", + "code": "", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "Cannot select more than 20 claims.\n Fix: Check the request body against the OpenAPI spec.", + "message": "Cannot select more than 20 claims.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "Cannot select more than 20 claims.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/verify__select_unknown_task_404.json b/tests/fixtures/parity/expected/verify__select_unknown_task_404.json new file mode 100644 index 0000000..68a468c --- /dev/null +++ b/tests/fixtures/parity/expected/verify__select_unknown_task_404.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Task not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Task not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Task not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Task not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json b/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json new file mode 100644 index 0000000..0c51d3c --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#cancelled", + "error": "Cancelled.", + "failure_class": "cancelled", + "failure_detail": "", + "failure_reason": "cancelled", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": false, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Cancelled.\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Cancelled.\",\n \"failure_reason\": \"cancelled\",\n \"failure_detail\": \"\",\n \"failure_class\": \"cancelled\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#cancelled\"\n}\n", + "wait": { + "cause": "Cancelled.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: Cancelled.\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: Cancelled.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: Cancelled.", + "status": 0 + } + }, + "request_id": "", + "retryable": false, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_completed.json b/tests/fixtures/parity/expected/verify__status_completed.json new file mode 100644 index 0000000..4455d97 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_completed.json @@ -0,0 +1,203 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "retryable": null, + "similar_claims": [], + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:21:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "wait": { + "returned": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + } + } +} diff --git a/tests/fixtures/parity/expected/verify__status_completed_after_a_while.json b/tests/fixtures/parity/expected/verify__status_completed_after_a_while.json new file mode 100644 index 0000000..4455d97 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_completed_after_a_while.json @@ -0,0 +1,203 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "retryable": null, + "similar_claims": [], + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:21:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "wait": { + "returned": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + } + } +} diff --git a/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..3b77489 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,203 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "retryable": null, + "similar_claims": [], + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "wait": { + "returned": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + } + } +} diff --git a/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..29f1593 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json @@ -0,0 +1,203 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "retryable": null, + "similar_claims": [], + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T08:00:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "wait": { + "returned": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + } + } +} diff --git a/tests/fixtures/parity/expected/verify__status_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/verify__status_completed_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..3b77489 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_completed_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,203 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "retryable": null, + "similar_claims": [], + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "wait": { + "returned": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + } + } +} diff --git a/tests/fixtures/parity/expected/verify__status_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/verify__status_completed_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..29f1593 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_completed_modified_at_same_day_hours_apart.json @@ -0,0 +1,203 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "retryable": null, + "similar_claims": [], + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T08:00:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "wait": { + "returned": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + } + } +} diff --git a/tests/fixtures/parity/expected/verify__status_failed.json b/tests/fixtures/parity/expected/verify__status_failed.json new file mode 100644 index 0000000..c379a8d --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_failed.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "error": "Pipeline stopped at: research_empty", + "failure_class": "insufficient_evidence", + "failure_detail": "", + "failure_reason": "research_empty", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": false, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Pipeline stopped at: research_empty\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped at: research_empty\",\n \"failure_reason\": \"research_empty\",\n \"failure_detail\": \"\",\n \"failure_class\": \"insufficient_evidence\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#insufficient-evidence\"\n}\n", + "wait": { + "cause": "Pipeline stopped at: research_empty", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: Pipeline stopped at: research_empty\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: Pipeline stopped at: research_empty", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: Pipeline stopped at: research_empty", + "status": 0 + } + }, + "request_id": "", + "retryable": false, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_failed_after_a_while.json b/tests/fixtures/parity/expected/verify__status_failed_after_a_while.json new file mode 100644 index 0000000..1de2af6 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_failed_after_a_while.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#internal", + "error": "Pipeline stopped: conclusion_failed.", + "failure_class": "internal", + "failure_detail": "", + "failure_reason": "conclusion_failed", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": false, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Pipeline stopped: conclusion_failed.\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped: conclusion_failed.\",\n \"failure_reason\": \"conclusion_failed\",\n \"failure_detail\": \"\",\n \"failure_class\": \"internal\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#internal\"\n}\n", + "wait": { + "cause": "Pipeline stopped: conclusion_failed.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "internal", + "failure_reason": "conclusion_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: Pipeline stopped: conclusion_failed.\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: Pipeline stopped: conclusion_failed.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: Pipeline stopped: conclusion_failed.", + "status": 0 + } + }, + "request_id": "", + "retryable": false, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_failed_after_a_while_framing.json b/tests/fixtures/parity/expected/verify__status_failed_after_a_while_framing.json new file mode 100644 index 0000000..52d9c84 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_failed_after_a_while_framing.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "error": "Pipeline stopped: framing_failed.", + "failure_class": "upstream_unavailable", + "failure_detail": "", + "failure_reason": "framing_failed", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": true, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Pipeline stopped: framing_failed.\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped: framing_failed.\",\n \"failure_reason\": \"framing_failed\",\n \"failure_detail\": \"\",\n \"failure_class\": \"upstream_unavailable\",\n \"retryable\": true,\n \"docs_url\": \"https://lenz.io/docs/errors#upstream-unavailable\"\n}\n", + "wait": { + "cause": "Pipeline stopped: framing_failed.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "upstream_unavailable", + "failure_reason": "framing_failed", + "fix": "Transient provider outage — retry the same request after a short wait.", + "friendly_text": "Pipeline failed: Pipeline stopped: framing_failed.\n Fix: Transient provider outage — retry the same request after a short wait.", + "hint": "", + "message": "Pipeline failed: Pipeline stopped: framing_failed.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Transient provider outage — retry the same request after a short wait.", + "message": "Pipeline failed: Pipeline stopped: framing_failed.", + "status": 0 + } + }, + "request_id": "", + "retryable": true, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_failed_retryable.json b/tests/fixtures/parity/expected/verify__status_failed_retryable.json new file mode 100644 index 0000000..fbe1bf9 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_failed_retryable.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "error": "Pipeline stopped at: adjudication_failed", + "failure_class": "upstream_unavailable", + "failure_detail": "", + "failure_reason": "adjudication_failed", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": true, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Pipeline stopped at: adjudication_failed\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped at: adjudication_failed\",\n \"failure_reason\": \"adjudication_failed\",\n \"failure_detail\": \"\",\n \"failure_class\": \"upstream_unavailable\",\n \"retryable\": true,\n \"docs_url\": \"https://lenz.io/docs/errors#upstream-unavailable\"\n}\n", + "wait": { + "cause": "Pipeline stopped at: adjudication_failed", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "upstream_unavailable", + "failure_reason": "adjudication_failed", + "fix": "Transient provider outage — retry the same request after a short wait.", + "friendly_text": "Pipeline failed: Pipeline stopped at: adjudication_failed\n Fix: Transient provider outage — retry the same request after a short wait.", + "hint": "", + "message": "Pipeline failed: Pipeline stopped at: adjudication_failed", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Transient provider outage — retry the same request after a short wait.", + "message": "Pipeline failed: Pipeline stopped at: adjudication_failed", + "status": 0 + } + }, + "request_id": "", + "retryable": true, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_needs_input.json b/tests/fixtures/parity/expected/verify__status_needs_input.json new file mode 100644 index 0000000..13a266b --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_needs_input.json @@ -0,0 +1,90 @@ +{ + "dump": { + "candidates": [], + "claims": [ + { + "domain": "Science", + "text": "The Earth is round." + }, + { + "domain": "Science", + "text": "Water boils at 100C at sea level." + } + ], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "multi_claim", + "result": null, + "retryable": null, + "similar_claims": [], + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "needs input (multi_claim)\nclaims found:\n 1. The Earth is round.\n 2. Water boils at 100C at sea level.\nresolve it: lenz verify --resume t --claim --detach\n", + "render_json": "{\n \"status\": \"needs_input\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"multi_claim\",\n \"hint\": \"The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [\n {\n \"text\": \"The Earth is round.\",\n \"domain\": \"Science\"\n },\n {\n \"text\": \"Water boils at 100C at sea level.\",\n \"domain\": \"Science\"\n }\n ],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "wait": { + "cause": "The verification needs caller input to proceed.", + "code": "", + "doc_url": "https://lenz.io/docs/verify#needs-input", + "fix": "Inspect the payload, then call client.select(task_id, texts=[...]) with the chosen claim(s).", + "friendly_text": "Pipeline paused: multi_claim\n Fix: Inspect the payload, then call client.select(task_id, texts=[...]) with the chosen claim(s).", + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "kind": "multi_claim", + "message": "Pipeline paused: multi_claim", + "payload": { + "candidates": [], + "claims": [ + { + "domain": "Science", + "text": "The Earth is round." + }, + { + "domain": "Science", + "text": "Water boils at 100C at sea level." + } + ], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "multi_claim", + "result": null, + "retryable": null, + "similar_claims": [], + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "payload_json": { + "error": { + "code": "needs_input", + "fix": "Inspect the payload, then call client.select(task_id, texts=[...]) with the chosen claim(s).", + "message": "Pipeline paused: multi_claim", + "status": 0 + } + }, + "request_id": "", + "status_code": 0, + "task_id": "t", + "type": "LenzNeedsInputError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_not_a_claim.json b/tests/fixtures/parity/expected/verify__status_not_a_claim.json new file mode 100644 index 0000000..5f7f1c5 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_not_a_claim.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "error": "Not a verifiable claim.", + "failure_class": "invalid_input", + "failure_detail": "", + "failure_reason": "not_a_claim", + "hint": "The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims.", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": false, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Not a verifiable claim.\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims.\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Not a verifiable claim.\",\n \"failure_reason\": \"not_a_claim\",\n \"failure_detail\": \"\",\n \"failure_class\": \"invalid_input\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#invalid-input\"\n}\n", + "wait": { + "cause": "Not a verifiable claim.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "invalid_input", + "failure_reason": "not_a_claim", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: Not a verifiable claim.\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims.", + "message": "Pipeline failed: Not a verifiable claim.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: Not a verifiable claim.", + "status": 0 + } + }, + "request_id": "", + "retryable": false, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_not_a_claim_after_a_while.json b/tests/fixtures/parity/expected/verify__status_not_a_claim_after_a_while.json new file mode 100644 index 0000000..422b2c4 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_not_a_claim_after_a_while.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "error": "Pipeline stopped: not_a_claim.", + "failure_class": "invalid_input", + "failure_detail": "", + "failure_reason": "not_a_claim", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": false, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Pipeline stopped: not_a_claim.\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped: not_a_claim.\",\n \"failure_reason\": \"not_a_claim\",\n \"failure_detail\": \"\",\n \"failure_class\": \"invalid_input\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#invalid-input\"\n}\n", + "wait": { + "cause": "Pipeline stopped: not_a_claim.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "invalid_input", + "failure_reason": "not_a_claim", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: Pipeline stopped: not_a_claim.\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: Pipeline stopped: not_a_claim.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: Pipeline stopped: not_a_claim.", + "status": 0 + } + }, + "request_id": "", + "retryable": false, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_not_yours_404.json b/tests/fixtures/parity/expected/verify__status_not_yours_404.json new file mode 100644 index 0000000..68a468c --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_not_yours_404.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Task not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Task not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Task not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Task not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_older_run_completed.json b/tests/fixtures/parity/expected/verify__status_older_run_completed.json new file mode 100644 index 0000000..4455d97 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_older_run_completed.json @@ -0,0 +1,203 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "retryable": null, + "similar_claims": [], + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:21:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "wait": { + "returned": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + } + } +} diff --git a/tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json b/tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json new file mode 100644 index 0000000..6700b6d --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#internal", + "error": "Pipeline failed.", + "failure_class": "internal", + "failure_detail": "", + "failure_reason": "task_error", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": false, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Pipeline failed.\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline failed.\",\n \"failure_reason\": \"task_error\",\n \"failure_detail\": \"\",\n \"failure_class\": \"internal\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#internal\"\n}\n", + "wait": { + "cause": "Pipeline failed.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "internal", + "failure_reason": "task_error", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: Pipeline failed.\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: Pipeline failed.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: Pipeline failed.", + "status": 0 + } + }, + "request_id": "", + "retryable": false, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json b/tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json new file mode 100644 index 0000000..c379a8d --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "error": "Pipeline stopped at: research_empty", + "failure_class": "insufficient_evidence", + "failure_detail": "", + "failure_reason": "research_empty", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": false, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Pipeline stopped at: research_empty\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped at: research_empty\",\n \"failure_reason\": \"research_empty\",\n \"failure_detail\": \"\",\n \"failure_class\": \"insufficient_evidence\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#insufficient-evidence\"\n}\n", + "wait": { + "cause": "Pipeline stopped at: research_empty", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: Pipeline stopped at: research_empty\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: Pipeline stopped at: research_empty", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: Pipeline stopped at: research_empty", + "status": 0 + } + }, + "request_id": "", + "retryable": false, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_older_run_in_progress.json b/tests/fixtures/parity/expected/verify__status_older_run_in_progress.json new file mode 100644 index 0000000..fb3142e --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_older_run_in_progress.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": 2, + "index": 2, + "poll_after_seconds": 5, + "step": "research", + "total": 5 + }, + "reason": "", + "result": null, + "retryable": null, + "similar_claims": [], + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "processing — Gathering evidence (2/5)\n", + "render_json": "{\n \"status\": \"processing\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"research\",\n \"index\": 2,\n \"total\": 5,\n \"elapsed_seconds\": 2,\n \"poll_after_seconds\": 5\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "wait": { + "cause": "unknown", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "", + "failure_reason": "", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: unknown\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: unknown", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: unknown", + "status": 0 + } + }, + "request_id": "", + "retryable": null, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_processing.json b/tests/fixtures/parity/expected/verify__status_processing.json new file mode 100644 index 0000000..fb3142e --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_processing.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": 2, + "index": 2, + "poll_after_seconds": 5, + "step": "research", + "total": 5 + }, + "reason": "", + "result": null, + "retryable": null, + "similar_claims": [], + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "processing — Gathering evidence (2/5)\n", + "render_json": "{\n \"status\": \"processing\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"research\",\n \"index\": 2,\n \"total\": 5,\n \"elapsed_seconds\": 2,\n \"poll_after_seconds\": 5\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "wait": { + "cause": "unknown", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "", + "failure_reason": "", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: unknown\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: unknown", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: unknown", + "status": 0 + } + }, + "request_id": "", + "retryable": null, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_processing_after_a_while.json b/tests/fixtures/parity/expected/verify__status_processing_after_a_while.json new file mode 100644 index 0000000..e7bb01d --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_processing_after_a_while.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "", + "progress": { + "elapsed_seconds": 2, + "index": 4, + "poll_after_seconds": 5, + "step": "adjudication", + "total": 5 + }, + "reason": "", + "result": null, + "retryable": null, + "similar_claims": [], + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "processing — Panel review across models (4/5)\n", + "render_json": "{\n \"status\": \"processing\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"adjudication\",\n \"index\": 4,\n \"total\": 5,\n \"elapsed_seconds\": 2,\n \"poll_after_seconds\": 5\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "wait": { + "cause": "unknown", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "", + "failure_reason": "", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: unknown\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: unknown", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: unknown", + "status": 0 + } + }, + "request_id": "", + "retryable": null, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_task_stuck.json b/tests/fixtures/parity/expected/verify__status_task_stuck.json new file mode 100644 index 0000000..d7e9b4b --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_task_stuck.json @@ -0,0 +1,51 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "error": "The task was never completed and has been marked failed.", + "failure_class": "upstream_unavailable", + "failure_detail": "", + "failure_reason": "task_stuck", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": true, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — The task was never completed and has been marked failed.\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"The task was never completed and has been marked failed.\",\n \"failure_reason\": \"task_stuck\",\n \"failure_detail\": \"\",\n \"failure_class\": \"upstream_unavailable\",\n \"retryable\": true,\n \"docs_url\": \"https://lenz.io/docs/errors#upstream-unavailable\"\n}\n", + "wait": { + "cause": "The task was never completed and has been marked failed.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "upstream_unavailable", + "failure_reason": "task_stuck", + "fix": "Transient provider outage — retry the same request after a short wait.", + "friendly_text": "Pipeline failed: The task was never completed and has been marked failed.\n Fix: Transient provider outage — retry the same request after a short wait.", + "hint": "", + "message": "Pipeline failed: The task was never completed and has been marked failed.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Transient provider outage — retry the same request after a short wait.", + "message": "Pipeline failed: The task was never completed and has been marked failed.", + "status": 0 + } + }, + "request_id": "", + "retryable": true, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_unknown_404.json b/tests/fixtures/parity/expected/verify__status_unknown_404.json new file mode 100644 index 0000000..68a468c --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_unknown_404.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Task not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Task not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Task not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Task not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__submit_202.json b/tests/fixtures/parity/expected/verify__submit_202.json new file mode 100644 index 0000000..4aa7f3a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__submit_202.json @@ -0,0 +1,9 @@ +{ + "dump": { + "chain_id": "78ac4e9f234ea3bd", + "claim_text": "", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__submit_202_options.json b/tests/fixtures/parity/expected/verify__submit_202_options.json new file mode 100644 index 0000000..4aa7f3a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__submit_202_options.json @@ -0,0 +1,9 @@ +{ + "dump": { + "chain_id": "78ac4e9f234ea3bd", + "claim_text": "", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__submit_202_text_alias.json b/tests/fixtures/parity/expected/verify__submit_202_text_alias.json new file mode 100644 index 0000000..4aa7f3a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__submit_202_text_alias.json @@ -0,0 +1,9 @@ +{ + "dump": { + "chain_id": "78ac4e9f234ea3bd", + "claim_text": "", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null +} diff --git a/tests/fixtures/parity/expected/verify__unauthenticated_401.json b/tests/fixtures/parity/expected/verify__unauthenticated_401.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__unauthenticated_401.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/verify__verification_200.json b/tests/fixtures/parity/expected/verify__verification_200.json new file mode 100644 index 0000000..9c9202f --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_200.json @@ -0,0 +1,92 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_200_covered.json b/tests/fixtures/parity/expected/verify__verification_200_covered.json new file mode 100644 index 0000000..9e542b8 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_200_covered.json @@ -0,0 +1,90 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": "2026-03-10T12:01:30+00:00", + "cap": 10000, + "certificate_id": "def7232d27eda03cf261569aee9948c5", + "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "currency": "EUR", + "reasons": [], + "status": "covered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"covered\",\n \"reasons\": [],\n \"certificate_id\": \"def7232d27eda03cf261569aee9948c5\",\n \"certificate_url\": \"https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5\",\n \"as_of\": \"2026-03-10T12:01:30+00:00\",\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/verify__verification_200_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..fc049f4 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_200_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,92 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_200_modified_at_null.json b/tests/fixtures/parity/expected/verify__verification_200_modified_at_null.json new file mode 100644 index 0000000..9c9202f --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_200_modified_at_null.json @@ -0,0 +1,92 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/verify__verification_200_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..11aaf1c --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_200_modified_at_same_day_hours_apart.json @@ -0,0 +1,92 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T08:00:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_200_modified_at_set.json b/tests/fixtures/parity/expected/verify__verification_200_modified_at_set.json new file mode 100644 index 0000000..fc049f4 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_200_modified_at_set.json @@ -0,0 +1,92 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_200_result_json_conclusion_only.json b/tests/fixtures/parity/expected/verify__verification_200_result_json_conclusion_only.json new file mode 100644 index 0000000..7df6e17 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_200_result_json_conclusion_only.json @@ -0,0 +1,53 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_con": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "debate_pro": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "panel_agreement": "split" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "", + "sources": [], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\n── Panel ──\nagreement: split\n\nDebate\n PRO \n CON \n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [],\n \"presumed_intent\": \"\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [],\n \"suggested_rewrite\": null,\n \"sources\": [],\n \"audit\": {\n \"adjudication_summary\": \"\",\n \"assessments\": [],\n \"debate_pro\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"debate_con\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"panel_agreement\": \"split\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_200_result_json_empty.json b/tests/fixtures/parity/expected/verify__verification_200_result_json_empty.json new file mode 100644 index 0000000..7df6e17 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_200_result_json_empty.json @@ -0,0 +1,53 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_con": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "debate_pro": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "panel_agreement": "split" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "", + "sources": [], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\n── Panel ──\nagreement: split\n\nDebate\n PRO \n CON \n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [],\n \"presumed_intent\": \"\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [],\n \"suggested_rewrite\": null,\n \"sources\": [],\n \"audit\": {\n \"adjudication_summary\": \"\",\n \"assessments\": [],\n \"debate_pro\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"debate_con\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"panel_agreement\": \"split\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_200_result_json_old_shape_strings.json b/tests/fixtures/parity/expected/verify__verification_200_result_json_old_shape_strings.json new file mode 100644 index 0000000..a8398f8 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_200_result_json_old_shape_strings.json @@ -0,0 +1,69 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "", + "assessments": [ + { + "focus_area": "", + "panelist_name": "Reviewer A", + "reasoning": "", + "score": null, + "warnings": [] + } + ], + "debate_con": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "debate_pro": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "panel_agreement": "split" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "", + "sources": [ + { + "date": "", + "snippet": "Old row.", + "source_name": "", + "title": "", + "url": "https://old.example/a" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (1):\n • https://old.example/a\n https://old.example/a\n Old row.\n\n── Panel ──\nagreement: split\n\nReviewer A\n\nDebate\n PRO \n CON \n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (1):\n • https://old.example/a\n https://old.example/a\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [],\n \"presumed_intent\": \"\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"\",\n \"title\": \"\",\n \"url\": \"https://old.example/a\",\n \"snippet\": \"Old row.\",\n \"date\": \"\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"\",\n \"score\": null,\n \"reasoning\": \"\",\n \"warnings\": []\n }\n ],\n \"debate_pro\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"debate_con\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"panel_agreement\": \"split\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_404.json b/tests/fixtures/parity/expected/verify__verification_404.json new file mode 100644 index 0000000..41573fe --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_404.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__verification_by_task_id_200.json b/tests/fixtures/parity/expected/verify__verification_by_task_id_200.json new file mode 100644 index 0000000..9c9202f --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_by_task_id_200.json @@ -0,0 +1,92 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_failed_409.json b/tests/fixtures/parity/expected/verify__verification_failed_409.json new file mode 100644 index 0000000..e938c8a --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_failed_409.json @@ -0,0 +1,26 @@ +{ + "error": { + "cause": "This check failed and has no result.", + "code": "verification_failed", + "doc_url": "https://lenz.io/docs/errors#insufficient-evidence", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "fix": "Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "friendly_text": "This check failed and has no result.\n Fix: Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "hint": "Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "message": "This check failed and has no result.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "message": "This check failed and has no result.", + "status": 409 + } + }, + "request_id": "", + "retryable": false, + "status_code": 409, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__verification_failed_409_not_a_claim_after_a_while.json b/tests/fixtures/parity/expected/verify__verification_failed_409_not_a_claim_after_a_while.json new file mode 100644 index 0000000..3a9e337 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_failed_409_not_a_claim_after_a_while.json @@ -0,0 +1,26 @@ +{ + "error": { + "cause": "This check failed and has no result.", + "code": "verification_failed", + "doc_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "not_a_claim", + "fix": "Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "friendly_text": "This check failed and has no result.\n Fix: Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "hint": "Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "message": "This check failed and has no result.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "message": "This check failed and has no result.", + "status": 409 + } + }, + "request_id": "", + "retryable": false, + "status_code": 409, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__verification_not_ready_409.json b/tests/fixtures/parity/expected/verify__verification_not_ready_409.json new file mode 100644 index 0000000..14d8183 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_not_ready_409.json @@ -0,0 +1,24 @@ +{ + "error": { + "cause": "This check is still running.", + "code": "verification_not_ready", + "doc_url": "https://lenz.io/docs/verify", + "fix": "Poll GET /verify/status/7cff18da2d979d6370e54a160be121ad until it completes, then read its result.", + "friendly_text": "This check is still running.\n Fix: Poll GET /verify/status/7cff18da2d979d6370e54a160be121ad until it completes, then read its result.", + "hint": "Poll GET /verify/status/7cff18da2d979d6370e54a160be121ad until it completes, then read its result.", + "message": "This check is still running.", + "payload_json": { + "error": { + "code": "not_ready", + "fix": "Poll GET /verify/status/7cff18da2d979d6370e54a160be121ad until it completes, then read its result.", + "message": "This check is still running.", + "status": 409 + } + }, + "request_id": "", + "status": "processing", + "status_code": 409, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "LenzVerificationNotReadyError" + } +} diff --git a/tests/fixtures/parity/expected/verify__verification_not_yours_404.json b/tests/fixtures/parity/expected/verify__verification_not_yours_404.json new file mode 100644 index 0000000..41573fe --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_not_yours_404.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__verification_public_anonymous.json b/tests/fixtures/parity/expected/verify__verification_public_anonymous.json new file mode 100644 index 0000000..ab58772 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_public_anonymous.json @@ -0,0 +1,80 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "public", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"public\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": null\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_public_of_another_user.json b/tests/fixtures/parity/expected/verify__verification_public_of_another_user.json new file mode 100644 index 0000000..d21e90e --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_public_of_another_user.json @@ -0,0 +1,92 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "public", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"public\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/verify__verification_unknown_task_404.json b/tests/fixtures/parity/expected/verify__verification_unknown_task_404.json new file mode 100644 index 0000000..41573fe --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_unknown_task_404.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__verification_zero_retention_expired_410.json b/tests/fixtures/parity/expected/verify__verification_zero_retention_expired_410.json new file mode 100644 index 0000000..99f659b --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_zero_retention_expired_410.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "This verification is no longer available: its account removes verifications after a set period.", + "code": "purged", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Its account's retention period removed it. A certificate issued for it is still available.", + "friendly_text": "This verification is no longer available: its account removes verifications after a set period.\n Fix: Its account's retention period removed it. A certificate issued for it is still available.", + "message": "This verification is no longer available: its account removes verifications after a set period.", + "payload_json": { + "error": { + "code": "gone", + "fix": "Its account's retention period removed it. A certificate issued for it is still available.", + "message": "This verification is no longer available: its account removes verifications after a set period.", + "status": 410 + } + }, + "purged_at": "2026-10-01T09:07:00.123456+00:00", + "request_id": "", + "status_code": 410, + "type": "LenzGoneError" + } +} diff --git a/tests/fixtures/parity/expected/verify__verification_zero_retention_served.json b/tests/fixtures/parity/expected/verify__verification_zero_retention_served.json new file mode 100644 index 0000000..e12b093 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_zero_retention_served.json @@ -0,0 +1,92 @@ +{ + "dump": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is well supported by evidence.", + "key_finding": "The Earth is approximately spherical.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical.\n\nThe claim is well supported by evidence.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical.\n\nThe claim is well supported by evidence.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical.\",\n \"executive_summary\": \"The claim is well supported by evidence.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" +} diff --git a/tests/fixtures/parity/expected/webhook__certificate_timestamped.json b/tests/fixtures/parity/expected/webhook__certificate_timestamped.json new file mode 100644 index 0000000..49e54c1 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__certificate_timestamped.json @@ -0,0 +1,23 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "coverage": { + "aggregate": 500000, + "as_of": "2026-09-03T10:00:00+00:00", + "cap": 10000, + "certificate_id": "def7232d27eda03cf261569aee9948c5", + "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "currency": "EUR", + "reasons": [], + "status": "covered", + "terms_version": "v1" + }, + "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "event": "certificate.timestamped", + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "type": "CertificateTimestamped", + "verification_id": "68ca4aaf" + } +} diff --git a/tests/fixtures/parity/expected/webhook__certificate_timestamped_review_child.json b/tests/fixtures/parity/expected/webhook__certificate_timestamped_review_child.json new file mode 100644 index 0000000..49e54c1 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__certificate_timestamped_review_child.json @@ -0,0 +1,23 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "coverage": { + "aggregate": 500000, + "as_of": "2026-09-03T10:00:00+00:00", + "cap": 10000, + "certificate_id": "def7232d27eda03cf261569aee9948c5", + "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "currency": "EUR", + "reasons": [], + "status": "covered", + "terms_version": "v1" + }, + "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "event": "certificate.timestamped", + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "type": "CertificateTimestamped", + "verification_id": "68ca4aaf" + } +} diff --git a/tests/fixtures/parity/expected/webhook__citecheck_completed.json b/tests/fixtures/parity/expected/webhook__citecheck_completed.json new file mode 100644 index 0000000..7044240 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__citecheck_completed.json @@ -0,0 +1,135 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "citecheck": { + "citation_failures": [], + "citation_issues": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": null, + "finding": "contradicted", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "position": null, + "quotes": [], + "rationale": "The page states it.", + "reference": "https://example.gov/report-2024", + "snippet": "The source says so.", + "source": "support", + "statement": "Unemployment fell to 4.1% in 2024." + } + ], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "contradicted", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": { + "finding": "contradicted", + "is_issue": true, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "98c0e7d5", + "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:28:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "issues_found", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 1, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "citecheck_id": "98c0e7d5", + "delivered_at": "2026-10-01T09:42:00.123456+00:00", + "event": "citecheck.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "status": "completed", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "type": "CitecheckEvent", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__citecheck_failed.json b/tests/fixtures/parity/expected/webhook__citecheck_failed.json new file mode 100644 index 0000000..aaa4c74 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__citecheck_failed.json @@ -0,0 +1,48 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "citecheck": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "citecheck_id": "98c0e7d5", + "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:28:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_citations", + "hint": "The text has no citation to check: no link, DOI or numbered reference with one.", + "retryable": false + }, + "language": "en", + "more_citations": null, + "outcome": "unchecked", + "policy": { + "max_citations": 20 + }, + "poll_after_seconds": null, + "status": "failed", + "summary": { + "citation_checks": null, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null + } + }, + "citecheck_id": "98c0e7d5", + "delivered_at": "2026-10-01T09:42:00.123456+00:00", + "event": "citecheck.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "status": "failed", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "type": "CitecheckEvent", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__review_completed.json b/tests/fixtures/parity/expected/webhook__review_completed.json new file mode 100644 index 0000000..4e39f6b --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__review_completed.json @@ -0,0 +1,157 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T10:03:00.123456+00:00", + "event": "review.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T09:56:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "task_id": "5ccac77f53ff49328ec134fb29d66542", + "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "verdict": "False", + "verification_id": "8ceed6f0", + "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", + "visibility": "private", + "warnings": [ + "w1" + ] + } + } + ], + "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:28:00.123456Z", + "credits": { + "charged": 11 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "verdict": "False", + "verification_id": "8ceed6f0", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "8cb2f001", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 1, + "failed": 0, + "planned": 1 + } + }, + "view": "full" + }, + "review_id": "8cb2f001", + "status": "completed", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "type": "ReviewEvent", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__review_completed_key_default_url.json b/tests/fixtures/parity/expected/webhook__review_completed_key_default_url.json new file mode 100644 index 0000000..61e1a22 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__review_completed_key_default_url.json @@ -0,0 +1,106 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T09:42:00.123456+00:00", + "event": "review.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:28:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "8cb2f001", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "review_id": "8cb2f001", + "status": "completed", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "type": "ReviewEvent", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__review_completed_oversized_rebuilt.json b/tests/fixtures/parity/expected/webhook__review_completed_oversized_rebuilt.json new file mode 100644 index 0000000..9fd5c6f --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__review_completed_oversized_rebuilt.json @@ -0,0 +1,15 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "", + "event": "review.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review": null, + "review_id": "8cb2f001", + "status": "completed", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "type": "ReviewEvent", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__review_failed.json b/tests/fixtures/parity/expected/webhook__review_failed.json new file mode 100644 index 0000000..08af731 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__review_failed.json @@ -0,0 +1,74 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T09:42:00.123456+00:00", + "event": "review.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:28:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_claim", + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "retryable": false + }, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": "unchecked", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "8cb2f001", + "status": "failed", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "review_id": "8cb2f001", + "status": "failed", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "type": "ReviewEvent", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_completed.json b/tests/fixtures/parity/expected/webhook__verification_completed.json new file mode 100644 index 0000000..aa45bf9 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_completed.json @@ -0,0 +1,103 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "event": "verification.completed", + "result": { + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "focus_area": "Evidence", + "panelist_name": "Reviewer A", + "reasoning": "Both sources place the tower in Paris.", + "score": 1.0, + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "focus_area": "Evidence", + "panelist_name": "Reviewer B", + "reasoning": "The encyclopedia entry is unambiguous.", + "score": 2.0, + "warnings": [] + } + ], + "debate_con": { + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source.", + "role": "con" + }, + "debate_pro": { + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable.", + "role": "pro" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "key_finding": "The Eiffel Tower is in Paris.", + "language": "en", + "lenz_score": 1, + "modified_at": null, + "presumed_intent": "Check where a landmark is.", + "sources": [ + { + "date": "2025-01-15", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower" + }, + { + "date": "", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris" + } + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "verdict": "False", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Only two sources were found." + ] + }, + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "type": "VerificationCompleted", + "verification_id": "68ca4aaf" + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_completed_in_batch.json b/tests/fixtures/parity/expected/webhook__verification_completed_in_batch.json new file mode 100644 index 0000000..e6dd56f --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_completed_in_batch.json @@ -0,0 +1,103 @@ +{ + "event": { + "attempt": 1, + "batch_id": "4ee0c3aa1b671eaa", + "delivered_at": "2026-10-01T09:35:00.123456+00:00", + "event": "verification.completed", + "result": { + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "focus_area": "Evidence", + "panelist_name": "Reviewer A", + "reasoning": "Both sources place the tower in Paris.", + "score": 1.0, + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "focus_area": "Evidence", + "panelist_name": "Reviewer B", + "reasoning": "The encyclopedia entry is unambiguous.", + "score": 2.0, + "warnings": [] + } + ], + "debate_con": { + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source.", + "role": "con" + }, + "debate_pro": { + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable.", + "role": "pro" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:28:00.123456+00:00", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "key_finding": "The Eiffel Tower is in Paris.", + "language": "en", + "lenz_score": 1, + "modified_at": null, + "presumed_intent": "Check where a landmark is.", + "sources": [ + { + "date": "2025-01-15", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower" + }, + { + "date": "", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris" + } + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "verdict": "False", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Only two sources were found." + ] + }, + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "type": "VerificationCompleted", + "verification_id": "68ca4aaf" + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..c050dad --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,103 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T09:35:00.123456+00:00", + "event": "verification.completed", + "result": { + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "focus_area": "Evidence", + "panelist_name": "Reviewer A", + "reasoning": "Both sources place the tower in Paris.", + "score": 1.0, + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "focus_area": "Evidence", + "panelist_name": "Reviewer B", + "reasoning": "The encyclopedia entry is unambiguous.", + "score": 2.0, + "warnings": [] + } + ], + "debate_con": { + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source.", + "role": "con" + }, + "debate_pro": { + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable.", + "role": "pro" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "key_finding": "The Eiffel Tower is in Paris.", + "language": "en", + "lenz_score": 1, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Check where a landmark is.", + "sources": [ + { + "date": "2025-01-15", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower" + }, + { + "date": "", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris" + } + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "verdict": "False", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Only two sources were found." + ] + }, + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "type": "VerificationCompleted", + "verification_id": "68ca4aaf" + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..78e81e6 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_same_day_hours_apart.json @@ -0,0 +1,103 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "event": "verification.completed", + "result": { + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "focus_area": "Evidence", + "panelist_name": "Reviewer A", + "reasoning": "Both sources place the tower in Paris.", + "score": 1.0, + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "focus_area": "Evidence", + "panelist_name": "Reviewer B", + "reasoning": "The encyclopedia entry is unambiguous.", + "score": 2.0, + "warnings": [] + } + ], + "debate_con": { + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source.", + "role": "con" + }, + "debate_pro": { + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable.", + "role": "pro" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "key_finding": "The Eiffel Tower is in Paris.", + "language": "en", + "lenz_score": 1, + "modified_at": null, + "presumed_intent": "Check where a landmark is.", + "sources": [ + { + "date": "2025-01-15", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower" + }, + { + "date": "", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris" + } + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "verdict": "False", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Only two sources were found." + ] + }, + "status": "completed", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "type": "VerificationCompleted", + "verification_id": "68ca4aaf" + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_failed_insufficient_evidence.json b/tests/fixtures/parity/expected/webhook__verification_failed_insufficient_evidence.json new file mode 100644 index 0000000..81bf11e --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_failed_insufficient_evidence.json @@ -0,0 +1,15 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T09:14:00.123456+00:00", + "error": "research_insufficient", + "event": "verification.failed", + "failure_class": "insufficient_evidence", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "VerificationFailed", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_failed_not_a_claim.json b/tests/fixtures/parity/expected/webhook__verification_failed_not_a_claim.json new file mode 100644 index 0000000..15e827d --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_failed_not_a_claim.json @@ -0,0 +1,15 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T09:14:00.123456+00:00", + "error": "not_a_claim", + "event": "verification.failed", + "failure_class": "invalid_input", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "VerificationFailed", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_failed_upstream_unavailable.json b/tests/fixtures/parity/expected/webhook__verification_failed_upstream_unavailable.json new file mode 100644 index 0000000..6ec1bcb --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_failed_upstream_unavailable.json @@ -0,0 +1,15 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T09:14:00.123456+00:00", + "error": "framing_failed", + "event": "verification.failed", + "failure_class": "upstream_unavailable", + "retryable": true, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "VerificationFailed", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_needs_input_multi_claim.json b/tests/fixtures/parity/expected/webhook__verification_needs_input_multi_claim.json new file mode 100644 index 0000000..55e2dc8 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_needs_input_multi_claim.json @@ -0,0 +1,27 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T09:14:00.123456+00:00", + "event": "verification.needs_input", + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "needs_input": { + "claims": [ + { + "domain": "", + "text": "The Earth is round." + }, + { + "domain": "Science", + "text": "The Moon is made of cheese." + } + ], + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "reason": "multi_claim" + }, + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "VerificationNeedsInput", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/legacy/account__library_empty.json b/tests/fixtures/parity/legacy/account__library_empty.json new file mode 100644 index 0000000..b419316 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__library_empty.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 0, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/account__library_item_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/account__library_item_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..e59c0d7 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__library_item_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "A library claim.", + "domain": "", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "language": "en" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/account__library_item_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/account__library_item_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..5fb5763 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__library_item_modified_at_same_day_hours_apart.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "A library claim.", + "domain": "", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "modified_at": null, + "language": "en" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/account__library_items.json b/tests/fixtures/parity/legacy/account__library_items.json new file mode 100644 index 0000000..2868a2e --- /dev/null +++ b/tests/fixtures/parity/legacy/account__library_items.json @@ -0,0 +1,60 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "Library claim 0.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "language": "en" + }, + { + "verification_id": "7c83247c", + "claim": "Library claim 1.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:28:00.123456+00:00", + "modified_at": null, + "language": "en" + }, + { + "verification_id": "b79019e4", + "claim": "Library claim 2.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:42:00.123456+00:00", + "modified_at": null, + "language": "en" + } + ], + "total": 3, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/account__library_no_match.json b/tests/fixtures/parity/legacy/account__library_no_match.json new file mode 100644 index 0000000..b419316 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__library_no_match.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 0, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/account__library_page_past_end.json b/tests/fixtures/parity/legacy/account__library_page_past_end.json new file mode 100644 index 0000000..86d64c7 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__library_page_past_end.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 1, + "page": 2, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/account__library_paginated_second_page.json b/tests/fixtures/parity/legacy/account__library_paginated_second_page.json new file mode 100644 index 0000000..9529811 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__library_paginated_second_page.json @@ -0,0 +1,45 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "Paged claim 020.", + "domain": "", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "language": "en" + }, + { + "verification_id": "7c83247c", + "claim": "Paged claim 021.", + "domain": "", + "entities": [], + "verdict": "True", + "confidence": "low", + "lenz_score": 10, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:28:00.123456+00:00", + "modified_at": null, + "language": "en" + } + ], + "total": 22, + "page": 2, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_extra_credits.json b/tests/fixtures/parity/legacy/account__me_usage_extra_credits.json new file mode 100644 index 0000000..e07e31f --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_extra_credits.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "credits": { + "total": 350, + "used": 0, + "remaining": 350, + "extra": 250, + "bonus": 250, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 0, + "quota_total": 35, + "quota_remaining": 35, + "bonus": 25, + "credits": 25, + "remaining": 35 + }, + "ask": { + "quota_used": 0, + "quota_total": 350, + "quota_remaining": 350, + "bonus": 250, + "credits": 250, + "remaining": 350 + }, + "assess": { + "quota_used": 0, + "quota_total": 350, + "quota_remaining": 350, + "bonus": 250, + "credits": 250, + "remaining": 350 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_extra_only.json b/tests/fixtures/parity/legacy/account__me_usage_extra_only.json new file mode 100644 index 0000000..0e00cac --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_extra_only.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "credits": { + "total": 137, + "used": 100, + "remaining": 37, + "extra": 37, + "bonus": 37, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 10, + "quota_total": 13, + "quota_remaining": 3, + "bonus": 3, + "credits": 3, + "remaining": 3 + }, + "ask": { + "quota_used": 100, + "quota_total": 137, + "quota_remaining": 37, + "bonus": 37, + "credits": 37, + "remaining": 37 + }, + "assess": { + "quota_used": 100, + "quota_total": 137, + "quota_remaining": 37, + "bonus": 37, + "credits": 37, + "remaining": 37 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_extract_calls.json b/tests/fixtures/parity/legacy/account__me_usage_extract_calls.json new file mode 100644 index 0000000..335d0f1 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_extract_calls.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "credits": { + "total": 100, + "used": 0, + "remaining": 100, + "extra": 0, + "bonus": 0, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 0, + "quota_total": 10, + "quota_remaining": 10, + "bonus": 0, + "credits": 0, + "remaining": 10 + }, + "ask": { + "quota_used": 0, + "quota_total": 100, + "quota_remaining": 100, + "bonus": 0, + "credits": 0, + "remaining": 100 + }, + "assess": { + "quota_used": 0, + "quota_total": 100, + "quota_remaining": 100, + "bonus": 0, + "credits": 0, + "remaining": 100 + }, + "extract": { + "calls_today": 7, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_free.json b/tests/fixtures/parity/legacy/account__me_usage_free.json new file mode 100644 index 0000000..6a23d7a --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_free.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "credits": { + "total": 100, + "used": 0, + "remaining": 100, + "extra": 0, + "bonus": 0, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 0, + "quota_total": 10, + "quota_remaining": 10, + "bonus": 0, + "credits": 0, + "remaining": 10 + }, + "ask": { + "quota_used": 0, + "quota_total": 100, + "quota_remaining": 100, + "bonus": 0, + "credits": 0, + "remaining": 100 + }, + "assess": { + "quota_used": 0, + "quota_total": 100, + "quota_remaining": 100, + "bonus": 0, + "credits": 0, + "remaining": 100 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_free_partly_spent.json b/tests/fixtures/parity/legacy/account__me_usage_free_partly_spent.json new file mode 100644 index 0000000..8f43c97 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_free_partly_spent.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "credits": { + "total": 100, + "used": 17, + "remaining": 83, + "extra": 0, + "bonus": 0, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 2, + "quota_total": 10, + "quota_remaining": 8, + "bonus": 0, + "credits": 0, + "remaining": 8 + }, + "ask": { + "quota_used": 17, + "quota_total": 100, + "quota_remaining": 83, + "bonus": 0, + "credits": 0, + "remaining": 83 + }, + "assess": { + "quota_used": 17, + "quota_total": 100, + "quota_remaining": 83, + "bonus": 0, + "credits": 0, + "remaining": 83 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_plus.json b/tests/fixtures/parity/legacy/account__me_usage_plus.json new file mode 100644 index 0000000..e49254d --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_plus.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "plus", + "plan_label": "Plus", + "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "credits": { + "total": 500, + "used": 0, + "remaining": 500, + "extra": 0, + "bonus": 0, + "resets_at": "2026-10-01T09:07:00.123456+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 0, + "quota_total": 50, + "quota_remaining": 50, + "bonus": 0, + "credits": 0, + "remaining": 50 + }, + "ask": { + "quota_used": 0, + "quota_total": 500, + "quota_remaining": 500, + "bonus": 0, + "credits": 0, + "remaining": 500 + }, + "assess": { + "quota_used": 0, + "quota_total": 500, + "quota_remaining": 500, + "bonus": 0, + "credits": 0, + "remaining": 500 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_pro.json b/tests/fixtures/parity/legacy/account__me_usage_pro.json new file mode 100644 index 0000000..4c0fab9 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_pro.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "pro", + "plan_label": "Pro", + "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "credits": { + "total": 5000, + "used": 0, + "remaining": 5000, + "extra": 0, + "bonus": 0, + "resets_at": "2026-10-01T09:07:00.123456+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 0, + "quota_total": 500, + "quota_remaining": 500, + "bonus": 0, + "credits": 0, + "remaining": 500 + }, + "ask": { + "quota_used": 0, + "quota_total": 5000, + "quota_remaining": 5000, + "bonus": 0, + "credits": 0, + "remaining": 5000 + }, + "assess": { + "quota_used": 0, + "quota_total": 5000, + "quota_remaining": 5000, + "bonus": 0, + "credits": 0, + "remaining": 5000 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_pro_extra.json b/tests/fixtures/parity/legacy/account__me_usage_pro_extra.json new file mode 100644 index 0000000..a15d613 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_pro_extra.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "pro", + "plan_label": "Pro", + "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "credits": { + "total": 6000, + "used": 0, + "remaining": 6000, + "extra": 1000, + "bonus": 1000, + "resets_at": "2026-10-01T09:07:00.123456+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 0, + "quota_total": 600, + "quota_remaining": 600, + "bonus": 100, + "credits": 100, + "remaining": 600 + }, + "ask": { + "quota_used": 0, + "quota_total": 6000, + "quota_remaining": 6000, + "bonus": 1000, + "credits": 1000, + "remaining": 6000 + }, + "assess": { + "quota_used": 0, + "quota_total": 6000, + "quota_remaining": 6000, + "bonus": 1000, + "credits": 1000, + "remaining": 6000 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_stale_quota_row.json b/tests/fixtures/parity/legacy/account__me_usage_stale_quota_row.json new file mode 100644 index 0000000..6a23d7a --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_stale_quota_row.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T09:07:00+00:00", + "credits": { + "total": 100, + "used": 0, + "remaining": 100, + "extra": 0, + "bonus": 0, + "resets_at": "2026-10-01T09:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 0, + "quota_total": 10, + "quota_remaining": 10, + "bonus": 0, + "credits": 0, + "remaining": 10 + }, + "ask": { + "quota_used": 0, + "quota_total": 100, + "quota_remaining": 100, + "bonus": 0, + "credits": 0, + "remaining": 100 + }, + "assess": { + "quota_used": 0, + "quota_total": 100, + "quota_remaining": 100, + "bonus": 0, + "credits": 0, + "remaining": 100 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + } +} diff --git a/tests/fixtures/parity/legacy/assess__401_no_key.json b/tests/fixtures/parity/legacy/assess__401_no_key.json new file mode 100644 index 0000000..8dd2fb4 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__401_no_key.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized" + } +} diff --git a/tests/fixtures/parity/legacy/assess__402_no_credits.json b/tests/fixtures/parity/legacy/assess__402_no_credits.json new file mode 100644 index 0000000..1cd3038 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__402_no_credits.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining /assess units.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/legacy/assess__422_blank_item.json b/tests/fixtures/parity/legacy/assess__422_blank_item.json new file mode 100644 index 0000000..af5a22a --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__422_blank_item.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims[1] is blank.", + "code": "blank_item" + } +} diff --git a/tests/fixtures/parity/legacy/assess__422_blank_text.json b/tests/fixtures/parity/legacy/assess__422_blank_text.json new file mode 100644 index 0000000..1ebb88e --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__422_blank_text.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Text is required." + } +} diff --git a/tests/fixtures/parity/legacy/assess__422_input_conflict.json b/tests/fixtures/parity/legacy/assess__422_input_conflict.json new file mode 100644 index 0000000..c811d89 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__422_input_conflict.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Send either `claim` (one text) or `claims` (a list of claims), not both.", + "code": "input_conflict" + } +} diff --git a/tests/fixtures/parity/legacy/assess__422_item_too_long.json b/tests/fixtures/parity/legacy/assess__422_item_too_long.json new file mode 100644 index 0000000..21e6f19 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__422_item_too_long.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.", + "code": "item_too_long" + } +} diff --git a/tests/fixtures/parity/legacy/assess__422_no_input_field.json b/tests/fixtures/parity/legacy/assess__422_no_input_field.json new file mode 100644 index 0000000..f862968 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__422_no_input_field.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/assess__422_too_many_items.json b/tests/fixtures/parity/legacy/assess__422_too_many_items.json new file mode 100644 index 0000000..80d556a --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__422_too_many_items.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "At most 20 claims per call; got 21. Split the list.", + "code": "too_many_items" + } +} diff --git a/tests/fixtures/parity/legacy/assess__422_wrong_type_claim.json b/tests/fixtures/parity/legacy/assess__422_wrong_type_claim.json new file mode 100644 index 0000000..d8d8c87 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__422_wrong_type_claim.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "string_type", + "loc": [ + "body", + "payload", + "claim" + ], + "msg": "Input should be a valid string" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/assess__422_wrong_type_claims.json b/tests/fixtures/parity/legacy/assess__422_wrong_type_claims.json new file mode 100644 index 0000000..a997f8c --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__422_wrong_type_claims.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "list_type", + "loc": [ + "body", + "payload", + "claims" + ], + "msg": "Input should be a valid list" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/assess__502_framing_failed.json b/tests/fixtures/parity/legacy/assess__502_framing_failed.json new file mode 100644 index 0000000..9b6d047 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__502_framing_failed.json @@ -0,0 +1,13 @@ +{ + "status": 502, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Framing failed", + "code": "framing_failed", + "error": "framing_failed" + } +} diff --git a/tests/fixtures/parity/legacy/assess__503_capacity.json b/tests/fixtures/parity/legacy/assess__503_capacity.json new file mode 100644 index 0000000..de01494 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__503_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "100", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 100, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/assess__503_list_all_upstream_unavailable.json b/tests/fixtures/parity/legacy/assess__503_list_all_upstream_unavailable.json new file mode 100644 index 0000000..9e74dd2 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__503_list_all_upstream_unavailable.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Our model providers are temporarily unavailable. Please retry shortly. Nothing was charged.", + "code": "upstream_unavailable", + "retry_after": 90, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/assess__503_upstream_unavailable.json b/tests/fixtures/parity/legacy/assess__503_upstream_unavailable.json new file mode 100644 index 0000000..b308e0c --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__503_upstream_unavailable.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Our model providers are temporarily unavailable. Please retry shortly.", + "code": "upstream_unavailable", + "retry_after": 90, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/assess__deep_tier_hit.json b/tests/fixtures/parity/legacy/assess__deep_tier_hit.json new file mode 100644 index 0000000..a5e696a --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__deep_tier_hit.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "Water boils at 100C.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": "https://lenz.io/api/v1/verifications/f3436f50", + "rationale": "The claim is verified.", + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__idempotency_body_mismatch.json b/tests/fixtures/parity/legacy/assess__idempotency_body_mismatch.json new file mode 100644 index 0000000..ef04562 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__idempotency_body_mismatch.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body." + } +} diff --git a/tests/fixtures/parity/legacy/assess__idempotency_first.json b/tests/fixtures/parity/legacy/assess__idempotency_first.json new file mode 100644 index 0000000..bac5c15 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__idempotency_first.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "The Earth orbits the Sun.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__idempotency_replay.json b/tests/fixtures/parity/legacy/assess__idempotency_replay.json new file mode 100644 index 0000000..bac5c15 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__idempotency_replay.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "The Earth orbits the Sun.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__list_all_error_rows.json b/tests/fixtures/parity/legacy/assess__list_all_error_rows.json new file mode 100644 index 0000000..ba846f5 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__list_all_error_rows.json @@ -0,0 +1,42 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "hi", + "language": "en", + "verdict": "Error", + "confidence": "low", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": "no_claim", + "candidate_claims": [], + "identified_claims": [], + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims." + }, + { + "claim": "hello", + "language": "en", + "verdict": "Error", + "confidence": "low", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": "no_claim", + "candidate_claims": [], + "identified_claims": [], + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims." + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__list_compound_item.json b/tests/fixtures/parity/legacy/assess__list_compound_item.json new file mode 100644 index 0000000..2a9d928 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__list_compound_item.json @@ -0,0 +1,45 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "Primary claim.", + "language": "en", + "verdict": "Mostly False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [ + "Second claim.", + "Third claim." + ], + "hint": "Assessed the main claim only. Send identified_claims as their own items to check the rest." + }, + { + "claim": "A plain claim.", + "language": "en", + "verdict": "Mostly False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__list_mixed_rows.json b/tests/fixtures/parity/legacy/assess__list_mixed_rows.json new file mode 100644 index 0000000..5c4bfe1 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__list_mixed_rows.json @@ -0,0 +1,70 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "Water boils at 100 C at sea level.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "hello there", + "language": "en", + "verdict": "Error", + "confidence": "low", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": "no_claim", + "candidate_claims": [], + "identified_claims": [], + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims." + }, + { + "claim": "vendor is down", + "language": "en", + "verdict": "Error", + "confidence": "low", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": "upstream_unavailable", + "candidate_claims": [], + "identified_claims": [], + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged." + }, + { + "claim": "cannot frame", + "language": "en", + "verdict": "Error", + "confidence": "low", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": "framing_failed", + "candidate_claims": [], + "identified_claims": [], + "hint": "This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; nothing was charged." + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__memo_hit.json b/tests/fixtures/parity/legacy/assess__memo_hit.json new file mode 100644 index 0000000..96a0c4d --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__memo_hit.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "The Danube flows through Vienna.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": "The river runs through the city.", + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__replay_later_200.json b/tests/fixtures/parity/legacy/assess__replay_later_200.json new file mode 100644 index 0000000..bac5c15 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__replay_later_200.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "The Earth orbits the Sun.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__single_no_claim.json b/tests/fixtures/parity/legacy/assess__single_no_claim.json new file mode 100644 index 0000000..b40abf7 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__single_no_claim.json @@ -0,0 +1,15 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [], + "error": "No verifiable claim detected", + "error_code": "no_claim", + "candidate_claims": [], + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__single_one_claim.json b/tests/fixtures/parity/legacy/assess__single_one_claim.json new file mode 100644 index 0000000..d27e7eb --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__single_one_claim.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "The registry reported 4,200 filings in 2024.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": "The registry lists 4,200 filings for 2024 and has not revised the figure.", + "dissent": "The registry figure is provisional, so the 2024 total cannot yet be confirmed.", + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__single_text_over_wave_more_claims.json b/tests/fixtures/parity/legacy/assess__single_text_over_wave_more_claims.json new file mode 100644 index 0000000..d4849fc --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__single_text_over_wave_more_claims.json @@ -0,0 +1,297 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "Claim number 0 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 1 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 2 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 3 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 4 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 5 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 6 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 7 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 8 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 9 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 10 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 11 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 12 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 13 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 14 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 15 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 16 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 17 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 18 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Claim number 19 is documented.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [ + "Claim number 20 is documented.", + "Claim number 21 is documented." + ] + } +} diff --git a/tests/fixtures/parity/legacy/assess__single_text_several_claims.json b/tests/fixtures/parity/legacy/assess__single_text_several_claims.json new file mode 100644 index 0000000..2db91ed --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__single_text_several_claims.json @@ -0,0 +1,56 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "Water boils at 100 C at sea level.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "The Moon is made of cheese.", + "language": "en", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "The Pacific is the deepest ocean.", + "language": "en", + "verdict": "Mostly True", + "confidence": "medium", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/assess__suggest_rewrite.json b/tests/fixtures/parity/legacy/assess__suggest_rewrite.json new file mode 100644 index 0000000..f4f7b54 --- /dev/null +++ b/tests/fixtures/parity/legacy/assess__suggest_rewrite.json @@ -0,0 +1,42 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "claims": [ + { + "claim": "Venus is the closest planet to the Sun.", + "language": "en", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": "Mercury, not Venus, is the closest planet to the Sun.", + "dissent": null, + "suggested_rewrite": "Mercury is the closest planet to the Sun.", + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + }, + { + "claim": "Mercury is the closest planet to the Sun.", + "language": "en", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": "Mercury is the innermost planet.", + "dissent": null, + "suggested_rewrite": null, + "error_code": null, + "candidate_claims": [], + "identified_claims": [], + "hint": null + } + ], + "error": null, + "more_claims": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__402_no_credits.json b/tests/fixtures/parity/legacy/citecheck__402_no_credits.json new file mode 100644 index 0000000..200f42b --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__402_no_credits.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining credits for citation checks.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 100, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 100, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_empty_body.json b/tests/fixtures/parity/legacy/citecheck__422_empty_body.json new file mode 100644 index 0000000..6f9d742 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_empty_body.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload: Value error, send exactly one of text and pairs", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload" + ], + "msg": "Value error, send exactly one of text and pairs" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_invalid_doi.json b/tests/fixtures/parity/legacy/citecheck__422_invalid_doi.json new file mode 100644 index 0000000..3026138 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_invalid_doi.json @@ -0,0 +1,24 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.pairs.0.doi: Send the DOI alone, like 10.1038/nature12373.", + "code": "invalid_doi", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0, + "doi" + ], + "msg": "Send the DOI alone, like 10.1038/nature12373." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_max_citations_over_20.json b/tests/fixtures/parity/legacy/citecheck__422_max_citations_over_20.json new file mode 100644 index 0000000..279eba0 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_max_citations_over_20.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.max_citations: Input should be less than or equal to 20", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "max_citations" + ], + "msg": "Input should be less than or equal to 20" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_max_citations_with_pairs.json b/tests/fixtures/parity/legacy/citecheck__422_max_citations_with_pairs.json new file mode 100644 index 0000000..bd0828b --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_max_citations_with_pairs.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload: Value error, max_citations goes with text: every pair is checked", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload" + ], + "msg": "Value error, max_citations goes with text: every pair is checked" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_pair_bad_url_scheme.json b/tests/fixtures/parity/legacy/citecheck__422_pair_bad_url_scheme.json new file mode 100644 index 0000000..c7046cc --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_pair_bad_url_scheme.json @@ -0,0 +1,24 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.pairs.0.url: Value error, url: an http or https link", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0, + "url" + ], + "msg": "Value error, url: an http or https link" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_pair_no_source.json b/tests/fixtures/parity/legacy/citecheck__422_pair_no_source.json new file mode 100644 index 0000000..10ddcdb --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_pair_no_source.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.pairs.0: Value error, send exactly one of url and doi", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0 + ], + "msg": "Value error, send exactly one of url and doi" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_pair_url_and_doi.json b/tests/fixtures/parity/legacy/citecheck__422_pair_url_and_doi.json new file mode 100644 index 0000000..10ddcdb --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_pair_url_and_doi.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.pairs.0: Value error, send exactly one of url and doi", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0 + ], + "msg": "Value error, send exactly one of url and doi" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_pairs_empty.json b/tests/fixtures/parity/legacy/citecheck__422_pairs_empty.json new file mode 100644 index 0000000..2df0724 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_pairs_empty.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.pairs: Value error, pairs: 1 to 20 items", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs" + ], + "msg": "Value error, pairs: 1 to 20 items" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_pairs_over_20.json b/tests/fixtures/parity/legacy/citecheck__422_pairs_over_20.json new file mode 100644 index 0000000..2df0724 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_pairs_over_20.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.pairs: Value error, pairs: 1 to 20 items", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs" + ], + "msg": "Value error, pairs: 1 to 20 items" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_quote_not_in_statement.json b/tests/fixtures/parity/legacy/citecheck__422_quote_not_in_statement.json new file mode 100644 index 0000000..462190c --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_quote_not_in_statement.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.pairs.0: Each quote must be words of its statement.", + "code": "quote_not_in_statement", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0 + ], + "msg": "Each quote must be words of its statement." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_text_and_pairs.json b/tests/fixtures/parity/legacy/citecheck__422_text_and_pairs.json new file mode 100644 index 0000000..6f9d742 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_text_and_pairs.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload: Value error, send exactly one of text and pairs", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload" + ], + "msg": "Value error, send exactly one of text and pairs" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_unknown_field.json b/tests/fixtures/parity/legacy/citecheck__422_unknown_field.json new file mode 100644 index 0000000..cb50c65 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_unknown_field.json @@ -0,0 +1,24 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.pairs.0.extra: Extra inputs are not permitted", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "pairs", + 0, + "extra" + ], + "msg": "Extra inputs are not permitted" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__422_url_input.json b/tests/fixtures/parity/legacy/citecheck__422_url_input.json new file mode 100644 index 0000000..d5ddd25 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_url_input.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Send the page's text, with its links, to check the sources it cites.", + "code": "url_input", + "errors": [ + { + "loc": [ + "body", + "text" + ], + "msg": "Send the page's text, with its links, to check the sources it cites." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__429_citecheck_in_flight.json b/tests/fixtures/parity/legacy/citecheck__429_citecheck_in_flight.json new file mode 100644 index 0000000..5222210 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__429_citecheck_in_flight.json @@ -0,0 +1,14 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This account already has 3 citation checks running. Wait for one to finish.", + "code": "citecheck_in_flight", + "retry_after_seconds": 60 + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__503_capacity.json b/tests/fixtures/parity/legacy/citecheck__503_capacity.json new file mode 100644 index 0000000..adcc209 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__503_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 90, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__503_citations_unavailable.json b/tests/fixtures/parity/legacy/citecheck__503_citations_unavailable.json new file mode 100644 index 0000000..ea4e7b8 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__503_citations_unavailable.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "300", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Citation checking is switched off right now. Retry later; nothing was charged.", + "code": "citations_unavailable", + "retry_after": 300, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_404_not_found.json b/tests/fixtures/parity/legacy/citecheck__get_404_not_found.json new file mode 100644 index 0000000..e0adfb3 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_404_not_found.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Citation check not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_404_review_id.json b/tests/fixtures/parity/legacy/citecheck__get_404_review_id.json new file mode 100644 index 0000000..e0adfb3 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_404_review_id.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Citation check not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_410_purged.json b/tests/fixtures/parity/legacy/citecheck__get_410_purged.json new file mode 100644 index 0000000..9fff981 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_410_purged.json @@ -0,0 +1,13 @@ +{ + "status": 410, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This check is no longer available: its account removes content after a set period.", + "code": "purged", + "purged_at": "2026-10-01T09:07:00.123456Z" + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_checking.json b/tests/fixtures/parity/legacy/citecheck__get_checking.json new file mode 100644 index 0000000..3c32986 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_checking.json @@ -0,0 +1,103 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "checking", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "poll_after_seconds": 10, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_clean.json b/tests/fixtures/parity/legacy/citecheck__get_completed_clean.json new file mode 100644 index 0000000..5e92112 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_clean.json @@ -0,0 +1,111 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_doi_pair.json b/tests/fixtures/parity/legacy/citecheck__get_completed_doi_pair.json new file mode 100644 index 0000000..2bdb1bf --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_doi_pair.json @@ -0,0 +1,76 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 1 + }, + "summary": { + "citations_found": 1, + "citations_selected": 1, + "citation_limit": 1, + "citation_limit_reached": false, + "citation_checks": { + "checked": 1, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 1 + }, + "citations": [ + { + "index": 0, + "reference": "10.1038/nature12373", + "cited_url": "https://doi.org/10.1038/nature12373", + "doi": "10.1038/nature12373", + "statement": "Diamond sensors measure heat in cells.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at None", + "page_published_date": null, + "page_language": "en", + "source_url": null, + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_incomplete.json b/tests/fixtures/parity/legacy/citecheck__get_completed_incomplete.json new file mode 100644 index 0000000..316b4d5 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_incomplete.json @@ -0,0 +1,127 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 1, + "unchecked": 0, + "failed": 1 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 1 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "The check failed on our side. Try again later.", + "failure": { + "hint": "The check failed on our side. Try again later.", + "docs_url": "https://lenz.io/docs/errors#internal", + "retryable": false, + "failure_class": "internal", + "failure_reason": "internal" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "hint": "The check failed on our side. Try again later.", + "docs_url": "https://lenz.io/docs/errors#internal", + "retryable": false, + "failure_class": "internal", + "failure_reason": "internal" + } + } + ], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_issues_found.json b/tests/fixtures/parity/legacy/citecheck__get_completed_issues_found.json new file mode 100644 index 0000000..75b7f7d --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_issues_found.json @@ -0,0 +1,141 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 20 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 1 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "contradicted", + "source": "support", + "is_issue": true + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "contradicted", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "finding": "contradicted", + "source": "support", + "snippet": "The source says so.", + "rationale": "The page states it.", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "failure": null + } + ], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_partly_supported.json b/tests/fixtures/parity/legacy/citecheck__get_completed_partly_supported.json new file mode 100644 index 0000000..5ae2366 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_partly_supported.json @@ -0,0 +1,111 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "partly_supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "partly_supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_unchecked.json b/tests/fixtures/parity/legacy/citecheck__get_completed_unchecked.json new file mode 100644 index 0000000..4e53bd4 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_unchecked.json @@ -0,0 +1,111 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 0, + "unchecked": 2, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "unchecked", + "source": null, + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "unchecked", + "snippet": null, + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": "no_text", + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": { + "finding": "unchecked", + "source": null, + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "unchecked", + "snippet": null, + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": "no_text", + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_failed_no_citations.json b/tests/fixtures/parity/legacy/citecheck__get_failed_no_citations.json new file mode 100644 index 0000000..c66a84e --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_failed_no_citations.json @@ -0,0 +1,42 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 20 + }, + "summary": { + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [], + "citation_issues": [], + "citation_failures": [], + "failure": { + "failure_reason": "no_citations", + "failure_class": "invalid_input", + "retryable": false, + "hint": "The text has no citation to check: no link, DOI or numbered reference with one.", + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_failed_upstream_unavailable.json b/tests/fixtures/parity/legacy/citecheck__get_failed_upstream_unavailable.json new file mode 100644 index 0000000..2fba44d --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_failed_upstream_unavailable.json @@ -0,0 +1,148 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 20 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 2 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": null, + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "Lenz could not reach this source just now. Try again later.", + "failure": { + "hint": "Lenz could not reach this source just now. Try again later.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "retryable": true, + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": null, + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "Lenz could not reach this source just now. Try again later.", + "failure": { + "hint": "Lenz could not reach this source just now. Try again later.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "retryable": true, + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "hint": "Lenz could not reach this source just now. Try again later.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "retryable": true, + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable" + } + }, + { + "citation_index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "hint": "Lenz could not reach this source just now. Try again later.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "retryable": true, + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable" + } + } + ], + "failure": { + "failure_reason": "upstream_unavailable", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_queued.json b/tests/fixtures/parity/legacy/citecheck__get_queued.json new file mode 100644 index 0000000..3e7d4d1 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_queued.json @@ -0,0 +1,72 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "queued", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "poll_after_seconds": 10, + "policy": { + "max_citations": 1 + }, + "summary": { + "citations_found": 1, + "citations_selected": 1, + "citation_limit": 1, + "citation_limit_reached": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_text_limit_exactly_at_limit.json b/tests/fixtures/parity/legacy/citecheck__get_text_limit_exactly_at_limit.json new file mode 100644 index 0000000..0ff4ff1 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_text_limit_exactly_at_limit.json @@ -0,0 +1,119 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_text_limit_reached.json b/tests/fixtures/parity/legacy/citecheck__get_text_limit_reached.json new file mode 100644 index 0000000..764e8b1 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_text_limit_reached.json @@ -0,0 +1,93 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 1 + }, + "summary": { + "citations_found": 2, + "citations_selected": 1, + "citation_limit": 1, + "citation_limit_reached": true, + "citation_checks": { + "checked": 1, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 1 + }, + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [ + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "sentence": "The agency said so in a statement.", + "position": { + "start": 111, + "end": 178, + "text": null + } + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__idempotency_body_mismatch_422.json b/tests/fixtures/parity/legacy/citecheck__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..fc47586 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__idempotency_body_mismatch_422.json @@ -0,0 +1,20 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch", + "errors": [ + { + "loc": [ + "header" + ], + "msg": "Idempotency-Key reused with a different request body." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__idempotency_conflict_409.json b/tests/fixtures/parity/legacy/citecheck__idempotency_conflict_409.json new file mode 100644 index 0000000..3978a83 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__idempotency_conflict_409.json @@ -0,0 +1,13 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A check with this Idempotency-Key is still being created. Retry shortly.", + "code": "idempotency_conflict", + "citecheck_id": null + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__idempotent_replay_202.json b/tests/fixtures/parity/legacy/citecheck__idempotent_replay_202.json new file mode 100644 index 0000000..864551e --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__idempotent_replay_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/citechecks/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__receipt_202.json b/tests/fixtures/parity/legacy/citecheck__receipt_202.json new file mode 100644 index 0000000..864551e --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__receipt_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/citechecks/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/legacy/citecheck__receipt_202_empty_webhook_url.json new file mode 100644 index 0000000..864551e --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__receipt_202_empty_webhook_url.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/citechecks/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/legacy/errors__ask_bad_language.json b/tests/fixtures/parity/legacy/errors__ask_bad_language.json new file mode 100644 index 0000000..2d6c4b1 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__ask_bad_language.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact." + } +} diff --git a/tests/fixtures/parity/legacy/errors__ask_empty_message.json b/tests/fixtures/parity/legacy/errors__ask_empty_message.json new file mode 100644 index 0000000..1eee180 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__ask_empty_message.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Message cannot be empty." + } +} diff --git a/tests/fixtures/parity/legacy/errors__ask_failed.json b/tests/fixtures/parity/legacy/errors__ask_failed.json new file mode 100644 index 0000000..6eb6b31 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__ask_failed.json @@ -0,0 +1,13 @@ +{ + "status": 502, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The chat model could not answer this question.", + "code": "ask_failed", + "error": "ask_failed" + } +} diff --git a/tests/fixtures/parity/legacy/errors__ask_idempotency_key_reused.json b/tests/fixtures/parity/legacy/errors__ask_idempotency_key_reused.json new file mode 100644 index 0000000..ef04562 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__ask_idempotency_key_reused.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body." + } +} diff --git a/tests/fixtures/parity/legacy/errors__ask_message_too_long.json b/tests/fixtures/parity/legacy/errors__ask_message_too_long.json new file mode 100644 index 0000000..659caed --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__ask_message_too_long.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Message too long (max 500 characters)." + } +} diff --git a/tests/fixtures/parity/legacy/errors__ask_not_completed.json b/tests/fixtures/parity/legacy/errors__ask_not_completed.json new file mode 100644 index 0000000..b59d747 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__ask_not_completed.json @@ -0,0 +1,11 @@ +{ + "status": 400, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Ask is only available for completed verifications." + } +} diff --git a/tests/fixtures/parity/legacy/errors__ask_reset_not_found.json b/tests/fixtures/parity/legacy/errors__ask_reset_not_found.json new file mode 100644 index 0000000..c0d18e1 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__ask_reset_not_found.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Verification not found." + } +} diff --git a/tests/fixtures/parity/legacy/errors__ask_send_not_found.json b/tests/fixtures/parity/legacy/errors__ask_send_not_found.json new file mode 100644 index 0000000..c0d18e1 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__ask_send_not_found.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Verification not found." + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_bad_key.json b/tests/fixtures/parity/legacy/errors__auth_bad_key.json new file mode 100644 index 0000000..8dd2fb4 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_bad_key.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_basic_scheme.json b/tests/fixtures/parity/legacy/errors__auth_basic_scheme.json new file mode 100644 index 0000000..8dd2fb4 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_basic_scheme.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_insufficient_scope.json b/tests/fixtures/parity/legacy/errors__auth_insufficient_scope.json new file mode 100644 index 0000000..15b8696 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_insufficient_scope.json @@ -0,0 +1,14 @@ +{ + "status": 403, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"verify\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "verify" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_missing_bearer.json b/tests/fixtures/parity/legacy/errors__auth_missing_bearer.json new file mode 100644 index 0000000..8dd2fb4 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_missing_bearer.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_missing_bearer_post.json b/tests/fixtures/parity/legacy/errors__auth_missing_bearer_post.json new file mode 100644 index 0000000..8dd2fb4 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_missing_bearer_post.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_history.json b/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_history.json new file mode 100644 index 0000000..e691e72 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_history.json @@ -0,0 +1,13 @@ +{ + "status": 403, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This report is private.", + "code": "private_claim", + "error": "private_claim" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_send.json b/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_send.json new file mode 100644 index 0000000..e691e72 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_send.json @@ -0,0 +1,13 @@ +{ + "status": 403, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This report is private.", + "code": "private_claim", + "error": "private_claim" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_unrecognised_prefix.json b/tests/fixtures/parity/legacy/errors__auth_unrecognised_prefix.json new file mode 100644 index 0000000..8dd2fb4 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_unrecognised_prefix.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized" + } +} diff --git a/tests/fixtures/parity/legacy/errors__http_error_library_bad_curated.json b/tests/fixtures/parity/legacy/errors__http_error_library_bad_curated.json new file mode 100644 index 0000000..03f1508 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__http_error_library_bad_curated.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Invalid curated list(s). Allowed: trivia" + } +} diff --git a/tests/fixtures/parity/legacy/errors__http_error_library_bad_verdict.json b/tests/fixtures/parity/legacy/errors__http_error_library_bad_verdict.json new file mode 100644 index 0000000..68f0346 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__http_error_library_bad_verdict.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True" + } +} diff --git a/tests/fixtures/parity/legacy/errors__not_found_ask.json b/tests/fixtures/parity/legacy/errors__not_found_ask.json new file mode 100644 index 0000000..c0d18e1 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__not_found_ask.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Verification not found." + } +} diff --git a/tests/fixtures/parity/legacy/errors__not_found_route.json b/tests/fixtures/parity/legacy/errors__not_found_route.json new file mode 100644 index 0000000..06110be --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__not_found_route.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "": "" + } +} diff --git a/tests/fixtures/parity/legacy/errors__not_found_verification.json b/tests/fixtures/parity/legacy/errors__not_found_verification.json new file mode 100644 index 0000000..3bf3204 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__not_found_verification.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Not found." + } +} diff --git a/tests/fixtures/parity/legacy/errors__not_found_verify_status.json b/tests/fixtures/parity/legacy/errors__not_found_verify_status.json new file mode 100644 index 0000000..09c1e79 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__not_found_verify_status.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Task not found." + } +} diff --git a/tests/fixtures/parity/legacy/errors__payment_required_ask.json b/tests/fixtures/parity/legacy/errors__payment_required_ask.json new file mode 100644 index 0000000..6648329 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__payment_required_ask.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining ask credits.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/legacy/errors__payment_required_assess.json b/tests/fixtures/parity/legacy/errors__payment_required_assess.json new file mode 100644 index 0000000..1cd3038 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__payment_required_assess.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining /assess units.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/legacy/errors__payment_required_verify.json b/tests/fixtures/parity/legacy/errors__payment_required_verify.json new file mode 100644 index 0000000..32f50ac --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__payment_required_verify.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining claim checks.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 10 + } +} diff --git a/tests/fixtures/parity/legacy/errors__payment_required_verify_batch_short.json b/tests/fixtures/parity/legacy/errors__payment_required_verify_batch_short.json new file mode 100644 index 0000000..1caec0e --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__payment_required_verify_batch_short.json @@ -0,0 +1,20 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Insufficient credits for batch.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "requested": 5, + "remaining": 3, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 30, + "cost": 50 + } +} diff --git a/tests/fixtures/parity/legacy/errors__rate_limited_extract.json b/tests/fixtures/parity/legacy/errors__rate_limited_extract.json new file mode 100644 index 0000000..9e6149c --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__rate_limited_extract.json @@ -0,0 +1,17 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "900", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "code": "extract_daily_limit", + "limit": 1000, + "reset_in_seconds": 900, + "doc_url": "https://lenz.io/docs/errors#rate-limits", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/legacy/errors__service_unavailable_ask.json b/tests/fixtures/parity/legacy/errors__service_unavailable_ask.json new file mode 100644 index 0000000..dac95ca --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__service_unavailable_ask.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The chat model is unavailable right now. Retry shortly.", + "code": "upstream_unavailable", + "retry_after": 90, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/errors__service_unavailable_capacity.json b/tests/fixtures/parity/legacy/errors__service_unavailable_capacity.json new file mode 100644 index 0000000..124f80e --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__service_unavailable_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 60, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/errors__validation_malformed_json.json b/tests/fixtures/parity/legacy/errors__validation_malformed_json.json new file mode 100644 index 0000000..0c5de40 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__validation_malformed_json.json @@ -0,0 +1,11 @@ +{ + "status": 400, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Cannot parse request body (Expecting property name enclosed in double quotes: line 1 column 2 (char 1))" + } +} diff --git a/tests/fixtures/parity/legacy/errors__validation_missing_field_hint.json b/tests/fixtures/parity/legacy/errors__validation_missing_field_hint.json new file mode 100644 index 0000000..86c632f --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__validation_missing_field_hint.json @@ -0,0 +1,27 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required" + } + ], + "hint": "Required field 'text' is missing. Unrecognised fields: content, bogus.", + "doc_url": "https://lenz.io/docs/errors#validation", + "unrecognized_fields": [ + "content", + "bogus" + ] + } +} diff --git a/tests/fixtures/parity/legacy/errors__validation_unknown_field_only.json b/tests/fixtures/parity/legacy/errors__validation_unknown_field_only.json new file mode 100644 index 0000000..98527f4 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__validation_unknown_field_only.json @@ -0,0 +1,26 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required" + } + ], + "hint": "Required field 'claim' is missing. Unrecognised fields: statement.", + "doc_url": "https://lenz.io/docs/errors#validation", + "unrecognized_fields": [ + "statement" + ] + } +} diff --git a/tests/fixtures/parity/legacy/errors__validation_unknown_field_wrong_type.json b/tests/fixtures/parity/legacy/errors__validation_unknown_field_wrong_type.json new file mode 100644 index 0000000..643f307 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__validation_unknown_field_wrong_type.json @@ -0,0 +1,25 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "list_type", + "loc": [ + "body", + "payload", + "claims" + ], + "msg": "Input should be a valid list" + } + ], + "unrecognized_fields": [ + "bogus_field", + "another" + ] + } +} diff --git a/tests/fixtures/parity/legacy/errors__validation_wrong_type.json b/tests/fixtures/parity/legacy/errors__validation_wrong_type.json new file mode 100644 index 0000000..0c8074f --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__validation_wrong_type.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "string_type", + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Input should be a valid string" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/extract__401_no_key.json b/tests/fixtures/parity/legacy/extract__401_no_key.json new file mode 100644 index 0000000..8dd2fb4 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__401_no_key.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized" + } +} diff --git a/tests/fixtures/parity/legacy/extract__422_blank_text.json b/tests/fixtures/parity/legacy/extract__422_blank_text.json new file mode 100644 index 0000000..1ebb88e --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__422_blank_text.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Text is required." + } +} diff --git a/tests/fixtures/parity/legacy/extract__422_focus_too_long.json b/tests/fixtures/parity/legacy/extract__422_focus_too_long.json new file mode 100644 index 0000000..5804588 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__422_focus_too_long.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "focus must be at most 300 characters of plain text." + } +} diff --git a/tests/fixtures/parity/legacy/extract__422_missing_text.json b/tests/fixtures/parity/legacy/extract__422_missing_text.json new file mode 100644 index 0000000..9e6d6c0 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__422_missing_text.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/extract__429_daily_limit.json b/tests/fixtures/parity/legacy/extract__429_daily_limit.json new file mode 100644 index 0000000..729effe --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__429_daily_limit.json @@ -0,0 +1,17 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "3600", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", + "code": "extract_daily_limit", + "limit": 1000, + "reset_in_seconds": 3600, + "doc_url": "https://lenz.io/docs/errors#rate-limits", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/legacy/extract__502_extraction_failed.json b/tests/fixtures/parity/legacy/extract__502_extraction_failed.json new file mode 100644 index 0000000..7deef02 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__502_extraction_failed.json @@ -0,0 +1,13 @@ +{ + "status": 502, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Extraction failed", + "code": "extraction_failed", + "error": "extraction_failed" + } +} diff --git a/tests/fixtures/parity/legacy/extract__503_upstream_unavailable.json b/tests/fixtures/parity/legacy/extract__503_upstream_unavailable.json new file mode 100644 index 0000000..b308e0c --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__503_upstream_unavailable.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Our model providers are temporarily unavailable. Please retry shortly.", + "code": "upstream_unavailable", + "retry_after": 90, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/extract__focus_ready.json b/tests/fixtures/parity/legacy/extract__focus_ready.json new file mode 100644 index 0000000..196fca1 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__focus_ready.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Beta fell 3% last year.", + "identified_claims": [], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": null + } +} diff --git a/tests/fixtures/parity/legacy/extract__idempotency_body_mismatch_422.json b/tests/fixtures/parity/legacy/extract__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..ef04562 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__idempotency_body_mismatch_422.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body." + } +} diff --git a/tests/fixtures/parity/legacy/extract__idempotency_replay.json b/tests/fixtures/parity/legacy/extract__idempotency_replay.json new file mode 100644 index 0000000..8fff1c2 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__idempotency_replay.json @@ -0,0 +1,31 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": null + } +} diff --git a/tests/fixtures/parity/legacy/extract__locate_all_dropped.json b/tests/fixtures/parity/legacy/extract__locate_all_dropped.json new file mode 100644 index 0000000..53117ce --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__locate_all_dropped.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "not_a_claim", + "claim": "", + "identified_claims": [], + "candidate_claims": [], + "domain": "", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": [] + } +} diff --git a/tests/fixtures/parity/legacy/extract__locate_failed.json b/tests/fixtures/parity/legacy/extract__locate_failed.json new file mode 100644 index 0000000..25c0403 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__locate_failed.json @@ -0,0 +1,31 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": null + } +} diff --git a/tests/fixtures/parity/legacy/extract__locate_leading_whitespace.json b/tests/fixtures/parity/legacy/extract__locate_leading_whitespace.json new file mode 100644 index 0000000..7479a3e --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__locate_leading_whitespace.json @@ -0,0 +1,39 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "start": 3, + "end": 25, + "text": "Alpha rose 5% in 2024." + } + ] + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/extract__locate_some_dropped.json b/tests/fixtures/parity/legacy/extract__locate_some_dropped.json new file mode 100644 index 0000000..6e51827 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__locate_some_dropped.json @@ -0,0 +1,52 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "start": 0, + "end": 22, + "text": "Alpha rose 5% in 2024." + } + ] + }, + { + "claim": "Beta fell 3% last year.", + "positions": [ + { + "start": 23, + "end": 46, + "text": "Beta fell 3% last year." + } + ] + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/extract__locate_true.json b/tests/fixtures/parity/legacy/extract__locate_true.json new file mode 100644 index 0000000..6e51827 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__locate_true.json @@ -0,0 +1,52 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "start": 0, + "end": 22, + "text": "Alpha rose 5% in 2024." + } + ] + }, + { + "claim": "Beta fell 3% last year.", + "positions": [ + { + "start": 23, + "end": 46, + "text": "Beta fell 3% last year." + } + ] + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/extract__no_match_with_focus.json b/tests/fixtures/parity/legacy/extract__no_match_with_focus.json new file mode 100644 index 0000000..0b34d79 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__no_match_with_focus.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "no_match", + "claim": "", + "identified_claims": [], + "candidate_claims": [], + "domain": "", + "key_entities": [], + "presumed_intent": "", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": null + } +} diff --git a/tests/fixtures/parity/legacy/extract__not_a_claim.json b/tests/fixtures/parity/legacy/extract__not_a_claim.json new file mode 100644 index 0000000..265efee --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__not_a_claim.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "not_a_claim", + "claim": "", + "identified_claims": [], + "candidate_claims": [], + "domain": "", + "key_entities": [], + "presumed_intent": "", + "original_input": "What a lovely day.", + "locations": null + } +} diff --git a/tests/fixtures/parity/legacy/extract__not_a_claim_beside_claims.json b/tests/fixtures/parity/legacy/extract__not_a_claim_beside_claims.json new file mode 100644 index 0000000..97f66d5 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__not_a_claim_beside_claims.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "not_a_claim", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024.", + "locations": null + } +} diff --git a/tests/fixtures/parity/legacy/extract__ready_one_claim.json b/tests/fixtures/parity/legacy/extract__ready_one_claim.json new file mode 100644 index 0000000..0a32c8b --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__ready_one_claim.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024.", + "locations": null + } +} diff --git a/tests/fixtures/parity/legacy/extract__ready_several_claims.json b/tests/fixtures/parity/legacy/extract__ready_several_claims.json new file mode 100644 index 0000000..25c0403 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__ready_several_claims.json @@ -0,0 +1,31 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": null + } +} diff --git a/tests/fixtures/parity/legacy/extract__replay_later_200.json b/tests/fixtures/parity/legacy/extract__replay_later_200.json new file mode 100644 index 0000000..8fff1c2 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__replay_later_200.json @@ -0,0 +1,31 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "locations": null + } +} diff --git a/tests/fixtures/parity/legacy/extract__url_fetch_502_unreadable.json b/tests/fixtures/parity/legacy/extract__url_fetch_502_unreadable.json new file mode 100644 index 0000000..7deef02 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__url_fetch_502_unreadable.json @@ -0,0 +1,13 @@ +{ + "status": 502, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Extraction failed", + "code": "extraction_failed", + "error": "extraction_failed" + } +} diff --git a/tests/fixtures/parity/legacy/extract__url_fetch_503_upstream.json b/tests/fixtures/parity/legacy/extract__url_fetch_503_upstream.json new file mode 100644 index 0000000..7cbd9f8 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__url_fetch_503_upstream.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The page could not be read right now. Please retry shortly.", + "code": "upstream_unavailable", + "retry_after": 90, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/extract__url_input_located.json b/tests/fixtures/parity/legacy/extract__url_input_located.json new file mode 100644 index 0000000..2cacb24 --- /dev/null +++ b/tests/fixtures/parity/legacy/extract__url_input_located.json @@ -0,0 +1,39 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "ready", + "claim": "Alpha rose 5% in 2024.", + "identified_claims": [], + "candidate_claims": [], + "domain": "Economics", + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "presumed_intent": "Verify reported figures", + "original_input": "https://example.com/an-article", + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "start": null, + "end": null, + "text": "Alpha rose 5% in 2024." + } + ] + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__401_no_credentials.json b/tests/fixtures/parity/legacy/review__401_no_credentials.json new file mode 100644 index 0000000..8dd2fb4 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__401_no_credentials.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized" + } +} diff --git a/tests/fixtures/parity/legacy/review__402_no_credits.json b/tests/fixtures/parity/legacy/review__402_no_credits.json new file mode 100644 index 0000000..c9adf9a --- /dev/null +++ b/tests/fixtures/parity/legacy/review__402_no_credits.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining credits to assess the draft.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 100, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 100, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/legacy/review__402_no_credits_exhausted.json b/tests/fixtures/parity/legacy/review__402_no_credits_exhausted.json new file mode 100644 index 0000000..cbcd2d1 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__402_no_credits_exhausted.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining credits to assess the draft.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 1 + } +} diff --git a/tests/fixtures/parity/legacy/review__422_blank_text.json b/tests/fixtures/parity/legacy/review__422_blank_text.json new file mode 100644 index 0000000..b820499 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_blank_text.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "text: send the draft, or one public http(s) URL.", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "text" + ], + "msg": "text: send the draft, or one public http(s) URL." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_citations_options_object.json b/tests/fixtures/parity/legacy/review__422_citations_options_object.json new file mode 100644 index 0000000..1f3923f --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_citations_options_object.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.citations: Extra inputs are not permitted", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "citations" + ], + "msg": "Extra inputs are not permitted" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_depth_unknown.json b/tests/fixtures/parity/legacy/review__422_depth_unknown.json new file mode 100644 index 0000000..dcce2ae --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_depth_unknown.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.escalate.depth: Input should be 'standard' or 'low'", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "depth" + ], + "msg": "Input should be 'standard' or 'low'" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_invalid_confidence_band.json b/tests/fixtures/parity/legacy/review__422_invalid_confidence_band.json new file mode 100644 index 0000000..8dccb97 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_invalid_confidence_band.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.escalate.confidence: Unknown confidence band 'certain'; use low, medium or high.", + "code": "invalid_confidence_band", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "confidence" + ], + "msg": "Unknown confidence band 'certain'; use low, medium or high." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_invalid_verdict_label.json b/tests/fixtures/parity/legacy/review__422_invalid_verdict_label.json new file mode 100644 index 0000000..40f409f --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_invalid_verdict_label.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.escalate.verdicts: Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True.", + "code": "invalid_verdict_label", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "verdicts" + ], + "msg": "Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_max_assessments_negative.json b/tests/fixtures/parity/legacy/review__422_max_assessments_negative.json new file mode 100644 index 0000000..c88a65e --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_max_assessments_negative.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.escalate.max_assessments: Input should be greater than or equal to 0", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_assessments" + ], + "msg": "Input should be greater than or equal to 0" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_max_citations_negative.json b/tests/fixtures/parity/legacy/review__422_max_citations_negative.json new file mode 100644 index 0000000..13e166c --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_max_citations_negative.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.escalate.max_citations: Input should be greater than or equal to 0", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_citations" + ], + "msg": "Input should be greater than or equal to 0" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_max_citations_too_big.json b/tests/fixtures/parity/legacy/review__422_max_citations_too_big.json new file mode 100644 index 0000000..10fcf2c --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_max_citations_too_big.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.escalate.max_citations: Input should be less than or equal to 20", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_citations" + ], + "msg": "Input should be less than or equal to 20" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_max_verifications_too_big.json b/tests/fixtures/parity/legacy/review__422_max_verifications_too_big.json new file mode 100644 index 0000000..9c2e2eb --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_max_verifications_too_big.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.escalate.max_verifications: Input should be less than or equal to 20", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "max_verifications" + ], + "msg": "Input should be less than or equal to 20" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_text_missing.json b/tests/fixtures/parity/legacy/review__422_text_missing.json new file mode 100644 index 0000000..de972a9 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_text_missing.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.text: Field required", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_text_wrong_type.json b/tests/fixtures/parity/legacy/review__422_text_wrong_type.json new file mode 100644 index 0000000..2638c63 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_text_wrong_type.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.text: Input should be a valid string", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Input should be a valid string" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_unknown_escalate_field.json b/tests/fixtures/parity/legacy/review__422_unknown_escalate_field.json new file mode 100644 index 0000000..2c5c7e9 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_unknown_escalate_field.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.escalate.nope: Extra inputs are not permitted", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "escalate", + "nope" + ], + "msg": "Extra inputs are not permitted" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_unknown_top_level_field.json b/tests/fixtures/parity/legacy/review__422_unknown_top_level_field.json new file mode 100644 index 0000000..f91e1f3 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_unknown_top_level_field.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.max_verifications: Extra inputs are not permitted", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "max_verifications" + ], + "msg": "Extra inputs are not permitted" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_unsupported_language.json b/tests/fixtures/parity/legacy/review__422_unsupported_language.json new file mode 100644 index 0000000..26f0a19 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_unsupported_language.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "language: Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "language" + ], + "msg": "language: Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__422_visibility_public.json b/tests/fixtures/parity/legacy/review__422_visibility_public.json new file mode 100644 index 0000000..3c72623 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_visibility_public.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "payload.visibility: Input should be 'private' or 'unlisted'", + "code": "validation_error", + "errors": [ + { + "loc": [ + "body", + "payload", + "visibility" + ], + "msg": "Input should be 'private' or 'unlisted'" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__429_extract_daily_limit.json b/tests/fixtures/parity/legacy/review__429_extract_daily_limit.json new file mode 100644 index 0000000..f7f182a --- /dev/null +++ b/tests/fixtures/parity/legacy/review__429_extract_daily_limit.json @@ -0,0 +1,17 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Daily /extract limit of 1000 reached for this account. A review of a URL reads the page like /extract and counts as one /extract call.", + "code": "extract_daily_limit", + "limit": 1000, + "reset_in_seconds": 60, + "doc_url": "https://lenz.io/docs/errors#rate-limits", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/legacy/review__429_review_in_flight.json b/tests/fixtures/parity/legacy/review__429_review_in_flight.json new file mode 100644 index 0000000..c8777a5 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__429_review_in_flight.json @@ -0,0 +1,14 @@ +{ + "status": 429, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This account already has 3 reviews running. Retry when one completes.", + "code": "review_in_flight", + "retry_after_seconds": 60 + } +} diff --git a/tests/fixtures/parity/legacy/review__503_capacity.json b/tests/fixtures/parity/legacy/review__503_capacity.json new file mode 100644 index 0000000..adcc209 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__503_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 90, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/review__503_citations_capacity.json b/tests/fixtures/parity/legacy/review__503_citations_capacity.json new file mode 100644 index 0000000..adcc209 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__503_citations_capacity.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": 90, + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/review__get_403_insufficient_scope.json b/tests/fixtures/parity/legacy/review__get_403_insufficient_scope.json new file mode 100644 index 0000000..80c529c --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_403_insufficient_scope.json @@ -0,0 +1,14 @@ +{ + "status": 403, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"history:read\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "history:read" + } +} diff --git a/tests/fixtures/parity/legacy/review__get_404_not_found.json b/tests/fixtures/parity/legacy/review__get_404_not_found.json new file mode 100644 index 0000000..8045cd8 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_404_not_found.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Review not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/legacy/review__get_404_other_account.json b/tests/fixtures/parity/legacy/review__get_404_other_account.json new file mode 100644 index 0000000..8045cd8 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_404_other_account.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Review not found.", + "code": "not_found" + } +} diff --git a/tests/fixtures/parity/legacy/review__get_410_purged.json b/tests/fixtures/parity/legacy/review__get_410_purged.json new file mode 100644 index 0000000..bedf6a5 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_410_purged.json @@ -0,0 +1,13 @@ +{ + "status": 410, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This review is no longer available: its account removes content after a set period.", + "code": "purged", + "purged_at": "2026-10-01T09:07:00.123456Z" + } +} diff --git a/tests/fixtures/parity/legacy/review__get_422_unknown_view.json b/tests/fixtures/parity/legacy/review__get_422_unknown_view.json new file mode 100644 index 0000000..531cbd7 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_422_unknown_view.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "view: use full or issues.", + "code": "validation_error", + "errors": [ + { + "loc": [ + "query", + "view" + ], + "msg": "view: use full or issues." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_assessing.json b/tests/fixtures/parity/legacy/review__get_assessing.json new file mode 100644 index 0000000..eff0c0d --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_assessing.json @@ -0,0 +1,110 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "assessing", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 0 + }, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": 10, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": null, + "assessment": { + "status": "running", + "verdict": null, + "confidence": null, + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": null, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": null, + "assessment": { + "status": "running", + "verdict": null, + "confidence": null, + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": null, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_assessment_rows_full_fields.json b/tests/fixtures/parity/legacy/review__get_assessment_rows_full_fields.json new file mode 100644 index 0000000..c7b40ba --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_assessment_rows_full_fields.json @@ -0,0 +1,170 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 0, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 1 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": null, + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "cap" + }, + "key_finding": null, + "rationale": "Two reviewers found the date wrong.", + "suggested_rewrite": "The EU AI Act entered into force in August 2024.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [ + { + "claim_index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "stage": "assessment", + "failure": { + "failure_reason": "timeout", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "The check ran out of time. Retry it; nothing was charged.", + "docs_url": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": "Two reviewers found the date wrong.", + "dissent": "One reviewer read the claim as about the entry into force.", + "verification_url": null, + "error_code": null, + "identified_claims": [ + "The EU AI Act took effect in 2024.", + "It took effect in March." + ], + "hint": "This text holds more than one claim.", + "suggested_rewrite": "The EU AI Act entered into force in August 2024.", + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "cap" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": null, + "assessment": { + "status": "failed", + "verdict": null, + "confidence": null, + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": "timeout", + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": { + "failure_reason": "timeout", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "The check ran out of time. Retry it; nothing was charged.", + "docs_url": null + } + }, + "escalation": null, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_clean.json b/tests/fixtures/parity/legacy/review__get_citations_clean.json new file mode 100644 index 0000000..c4bb6cc --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_clean.json @@ -0,0 +1,183 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_failed_and_unchecked.json b/tests/fixtures/parity/legacy/review__get_citations_failed_and_unchecked.json new file mode 100644 index 0000000..39737a0 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_failed_and_unchecked.json @@ -0,0 +1,199 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 0, + "unchecked": 1, + "failed": 1 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "hint": "The check failed on our side. Try again later.", + "docs_url": "https://lenz.io/docs/errors#internal", + "retryable": false, + "failure_class": "internal", + "failure_reason": "internal" + } + } + ], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "The check failed on our side. Try again later.", + "failure": { + "hint": "The check failed on our side. Try again later.", + "docs_url": "https://lenz.io/docs/errors#internal", + "retryable": false, + "failure_class": "internal", + "failure_reason": "internal" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "unchecked", + "source": null, + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "unchecked", + "snippet": null, + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": "no_text", + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_issue_full.json b/tests/fixtures/parity/legacy/review__get_citations_issue_full.json new file mode 100644 index 0000000..df60b05 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_issue_full.json @@ -0,0 +1,205 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 1, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "finding": "contradicted", + "source": "support", + "snippet": "The source says so.", + "rationale": "The page states it.", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "failure": null + } + ], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "contradicted", + "source": "support", + "is_issue": true + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "contradicted", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_issue_issues_view.json b/tests/fixtures/parity/legacy/review__get_citations_issue_issues_view.json new file mode 100644 index 0000000..c8414d9 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_issue_issues_view.json @@ -0,0 +1,93 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "issues", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 1, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [ + { + "citation_index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "finding": "contradicted", + "source": "support", + "snippet": "The source says so.", + "rationale": "The page states it.", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "failure": null + } + ], + "citation_failures": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_limit_exactly_at_limit.json b/tests/fixtures/parity/legacy/review__get_citations_limit_exactly_at_limit.json new file mode 100644 index 0000000..9d9e9d7 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_limit_exactly_at_limit.json @@ -0,0 +1,183 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 2, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_limit_reached.json b/tests/fixtures/parity/legacy/review__get_citations_limit_reached.json new file mode 100644 index 0000000..570c334 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_limit_reached.json @@ -0,0 +1,157 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 1, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 1, + "citation_limit": 1, + "citation_limit_reached": true, + "citation_checks": { + "checked": 1, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [ + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "sentence": "The agency said so in a statement.", + "position": { + "start": 111, + "end": 178, + "text": null + } + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_no_claim_but_citations.json b/tests/fixtures/parity/legacy/review__get_citations_no_claim_but_citations.json new file mode 100644 index 0000000..5c19752 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_no_claim_but_citations.json @@ -0,0 +1,152 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": 0, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_no_claim_no_citation.json b/tests/fixtures/parity/legacy/review__get_citations_no_claim_no_citation.json new file mode 100644 index 0000000..c588505 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_no_claim_no_citation.json @@ -0,0 +1,68 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": null, + "claim_limit": 20, + "claim_limit_reached": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "failure_reason": "no_claim", + "failure_class": "invalid_input", + "retryable": false, + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_not_asked.json b/tests/fixtures/parity/legacy/review__get_citations_not_asked.json new file mode 100644 index 0000000..386fe10 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_not_asked.json @@ -0,0 +1,100 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_only.json b/tests/fixtures/parity/legacy/review__get_citations_only.json new file mode 100644 index 0000000..287e33e --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_only.json @@ -0,0 +1,159 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 0, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": 0, + "claim_limit": 0, + "claim_limit_reached": true, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [ + "The EU AI Act took effect in March 2024." + ], + "more_claim_locations": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "positions": null + } + ], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_partly_supported.json b/tests/fixtures/parity/legacy/review__get_citations_partly_supported.json new file mode 100644 index 0000000..6c96719 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_partly_supported.json @@ -0,0 +1,183 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 3 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry.", + "quotes": [], + "position": { + "start": 0, + "end": 110, + "text": null + }, + "result": { + "finding": "partly_supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "partly_supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": { + "start": 111, + "end": 178, + "text": null + }, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_running.json b/tests/fixtures/parity/legacy/review__get_citations_running.json new file mode 100644 index 0000000..e72b98c --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_running.json @@ -0,0 +1,167 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "verifying", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 20, + "citation_limit_reached": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": 15, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [ + { + "index": 0, + "reference": "a report from the ministry", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": null, + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "a statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": null, + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "pending", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_skipped_switched_off.json b/tests/fixtures/parity/legacy/review__get_citations_skipped_switched_off.json new file mode 100644 index 0000000..b67e879 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_skipped_switched_off.json @@ -0,0 +1,100 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": "switched_off" + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_citations_skipped_url_input.json b/tests/fixtures/parity/legacy/review__get_citations_skipped_url_input.json new file mode 100644 index 0000000..04d13c0 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_citations_skipped_url_input.json @@ -0,0 +1,68 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 20, + "suggest_edits": false + }, + "summary": { + "claims_selected": null, + "claim_limit": 20, + "claim_limit_reached": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": "url_input" + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "failure_reason": "upstream_unavailable", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/legacy/review__get_claim_limit_not_reached.json b/tests/fixtures/parity/legacy/review__get_claim_limit_not_reached.json new file mode 100644 index 0000000..97b6112 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_claim_limit_not_reached.json @@ -0,0 +1,130 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 3, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 3, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_claim_limit_reached_exact.json b/tests/fixtures/parity/legacy/review__get_claim_limit_reached_exact.json new file mode 100644 index 0000000..55d5182 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_claim_limit_reached_exact.json @@ -0,0 +1,130 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 2, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 2, + "claim_limit_reached": true, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_completed_assess_only.json b/tests/fixtures/parity/legacy/review__get_completed_assess_only.json new file mode 100644 index 0000000..ff8ce84 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_completed_assess_only.json @@ -0,0 +1,156 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 0, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": null, + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "cap" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "cap" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_completed_clean.json b/tests/fixtures/parity/legacy/review__get_completed_clean.json new file mode 100644 index 0000000..3f2020c --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_completed_clean.json @@ -0,0 +1,130 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_completed_issues_all_verified.json b/tests/fixtures/parity/legacy/review__get_completed_issues_all_verified.json new file mode 100644 index 0000000..81957e3 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_completed_issues_all_verified.json @@ -0,0 +1,261 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 3, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 3, + "failed": 0 + }, + "verifications": { + "planned": 2, + "completed": 2, + "failed": 0 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 23 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "failure": null, + "suggested_edits": null + }, + { + "claim_index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b79019e4", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", + "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "Mostly False", + "confidence": "medium", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "c010dca9a43543f518e49f4ec7842ceb", + "verification_id": "b4e2e99d", + "claim": "The EU AI Act took effect in March 2024.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "executive_summary": "Summary for The EU AI Act took effect in March 2024.", + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:49:00.123456+00:00", + "modified_at": null, + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "failure": null + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "4634b89821fd877f1edf8aaeb82b7ee5", + "verification_id": "b79019e4", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:49:00.123456+00:00", + "modified_at": null, + "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", + "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "failure": null + }, + "suggested_edits": null + }, + { + "index": 2, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_completed_issues_full.json b/tests/fixtures/parity/legacy/review__get_completed_issues_full.json new file mode 100644 index 0000000..a8c7245 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_completed_issues_full.json @@ -0,0 +1,312 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 4, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 3, + "failed": 1 + }, + "verifications": { + "planned": 2, + "completed": 1, + "failed": 1 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 13 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "failure": null, + "suggested_edits": null + }, + { + "claim_index": 2, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "failure_reason": "research_empty", + "failure_class": "insufficient_evidence", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + }, + "suggested_edits": null + } + ], + "failures": [ + { + "claim_index": 1, + "claim": "Water boils at 100 C at sea level.", + "stage": "assessment", + "failure": { + "failure_reason": "upstream_unavailable", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } + ], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "Mostly False", + "confidence": "medium", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "7df4009bc27aab64ce5d5a01fbafe599", + "verification_id": "b4e2e99d", + "claim": "The EU AI Act took effect in March 2024.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "executive_summary": "Summary for The EU AI Act took effect in March 2024.", + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:42:00.123456+00:00", + "modified_at": null, + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "failure": null + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": null, + "assessment": { + "status": "failed", + "verdict": null, + "confidence": null, + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": "upstream_unavailable", + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": { + "failure_reason": "upstream_unavailable", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + "escalation": null, + "verification": null, + "suggested_edits": null + }, + { + "index": 2, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "verification_id": null, + "task_id": "5ccac77f53ff49328ec134fb29d66542", + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "modified_at": null, + "verification_url": null, + "url": null, + "failure": { + "failure_reason": "research_empty", + "failure_class": "insufficient_evidence", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + }, + "status": "failed", + "content_status": "available" + }, + "suggested_edits": null + }, + { + "index": 3, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_completed_issues_view_issues.json b/tests/fixtures/parity/legacy/review__get_completed_issues_view_issues.json new file mode 100644 index 0000000..c42387e --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_completed_issues_view_issues.json @@ -0,0 +1,133 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "issues", + "status": "completed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 4, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 3, + "failed": 1 + }, + "verifications": { + "planned": 2, + "completed": 1, + "failed": 1 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 13 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "failure": null, + "suggested_edits": null + }, + { + "claim_index": 2, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "failure_reason": "research_empty", + "failure_class": "insufficient_evidence", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + }, + "suggested_edits": null + } + ], + "failures": [ + { + "claim_index": 1, + "claim": "Water boils at 100 C at sea level.", + "stage": "assessment", + "failure": { + "failure_reason": "upstream_unavailable", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_completed_positions.json b/tests/fixtures/parity/legacy/review__get_completed_positions.json new file mode 100644 index 0000000..85f1f60 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_completed_positions.json @@ -0,0 +1,142 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": 0, + "end": 40, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": [ + { + "start": 55, + "end": 89, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_completed_recovered_claim.json b/tests/fixtures/parity/legacy/review__get_completed_recovered_claim.json new file mode 100644 index 0000000..9bba88a --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_completed_recovered_claim.json @@ -0,0 +1,100 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "Orbit Motors stock increased by 40% in 2022.", + "positions": [], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_completed_with_untraced_claim.json b/tests/fixtures/parity/legacy/review__get_completed_with_untraced_claim.json new file mode 100644 index 0000000..c58b50b --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_completed_with_untraced_claim.json @@ -0,0 +1,142 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": 0, + "end": 40, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": [ + { + "start": 50, + "end": 84, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_during_quick_check.json b/tests/fixtures/parity/legacy/review__get_during_quick_check.json new file mode 100644 index 0000000..7b3787b --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_during_quick_check.json @@ -0,0 +1,88 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "assessing", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 0 + }, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": 10, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": null, + "assessment": { + "status": "running", + "verdict": null, + "confidence": null, + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": null, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_failed_every_assessment_failed.json b/tests/fixtures/parity/legacy/review__get_failed_every_assessment_failed.json new file mode 100644 index 0000000..12698ba --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_failed_every_assessment_failed.json @@ -0,0 +1,157 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 0, + "failed": 2 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "stage": "assessment", + "failure": { + "failure_reason": "upstream_unavailable", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + { + "claim_index": 1, + "claim": "Water boils at 100 C at sea level.", + "stage": "assessment", + "failure": { + "failure_reason": "upstream_unavailable", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + } + ], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": null, + "assessment": { + "status": "failed", + "verdict": null, + "confidence": null, + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": "upstream_unavailable", + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": { + "failure_reason": "upstream_unavailable", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + "escalation": null, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": null, + "assessment": { + "status": "failed", + "verdict": null, + "confidence": null, + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": "upstream_unavailable", + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": { + "failure_reason": "upstream_unavailable", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } + }, + "escalation": null, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": { + "failure_reason": "assessment_failed", + "failure_class": "upstream_unavailable", + "retryable": true, + "hint": "No claim could be assessed. Retry the review with a new Idempotency-Key; nothing was charged.", + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_failed_no_claim.json b/tests/fixtures/parity/legacy/review__get_failed_no_claim.json new file mode 100644 index 0000000..5d5200a --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_failed_no_claim.json @@ -0,0 +1,68 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": null, + "claim_limit": 20, + "claim_limit_reached": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "failure_reason": "no_claim", + "failure_class": "invalid_input", + "retryable": false, + "hint": "The text is a greeting, not a statement about the world. Send one factual claim, or run the text through /extract to enumerate its claims.", + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/legacy/review__get_failed_no_claim_untraced.json b/tests/fixtures/parity/legacy/review__get_failed_no_claim_untraced.json new file mode 100644 index 0000000..823468b --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_failed_no_claim_untraced.json @@ -0,0 +1,68 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": null, + "claim_limit": 20, + "claim_limit_reached": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "failure_reason": "no_claim", + "failure_class": "invalid_input", + "retryable": false, + "hint": "None of the claims found in the text could be traced back to it. Send one factual claim, or run the text through /extract to enumerate its claims.", + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/legacy/review__get_more_claims.json b/tests/fixtures/parity/legacy/review__get_more_claims.json new file mode 100644 index 0000000..5f268da --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_more_claims.json @@ -0,0 +1,155 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 2, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 2, + "claim_limit_reached": true, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": 0, + "end": 40, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": [ + { + "start": 50, + "end": 84, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [ + "The Eiffel Tower is in Berlin." + ], + "more_claim_locations": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "positions": [ + { + "start": 91, + "end": 121, + "text": "The Eiffel Tower is in Berlin." + } + ] + } + ], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..52e321f --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,151 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 1, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 11 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "7df4009bc27aab64ce5d5a01fbafe599", + "verification_id": "b4e2e99d", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T23:55:00.123456+00:00", + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "failure": null + }, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..9054ce3 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_same_day_hours_apart.json @@ -0,0 +1,151 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 1, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 11 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "b4e2e99d", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "7df4009bc27aab64ce5d5a01fbafe599", + "verification_id": "b4e2e99d", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T08:00:00.123456+00:00", + "modified_at": null, + "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "failure": null + }, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_policy_resolved.json b/tests/fixtures/parity/legacy/review__get_policy_resolved.json new file mode 100644 index 0000000..3116b84 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_policy_resolved.json @@ -0,0 +1,98 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 2, + "depth": "low", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_queued.json b/tests/fixtures/parity/legacy/review__get_queued.json new file mode 100644 index 0000000..306403b --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_queued.json @@ -0,0 +1,62 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "queued", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": null, + "claim_limit": 20, + "claim_limit_reached": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": 10, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": null, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + } +} diff --git a/tests/fixtures/parity/legacy/review__get_suggested_edits.json b/tests/fixtures/parity/legacy/review__get_suggested_edits.json new file mode 100644 index 0000000..aba4758 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_suggested_edits.json @@ -0,0 +1,269 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "verifying", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "suggest_edits": true, + "max_citations": 0 + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 2, + "completed": 1, + "failed": 0 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 12 + }, + "poll_after_seconds": 15, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "7c83247c", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": { + "status": "completed", + "edits": [ + { + "position": 0, + "start": 23, + "end": 29, + "text": "Berlin", + "replacement": "Paris" + } + ] + } + }, + { + "claim_index": 1, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": null, + "suggested_edits": { + "status": "pending", + "edits": null + } + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": [ + { + "start": 0, + "end": 30, + "text": "The Eiffel Tower is in Berlin." + } + ], + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "9bca1e0819de60a750186e812dc58e2b", + "verification_id": "7c83247c", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:35:00.123456+00:00", + "modified_at": null, + "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "failure": null + }, + "suggested_edits": { + "status": "completed", + "edits": [ + { + "position": 0, + "start": 23, + "end": 29, + "text": "Berlin", + "replacement": "Paris" + } + ] + } + }, + { + "index": 1, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": 45, + "end": 85, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "processing", + "content_status": "available", + "verification_id": null, + "task_id": null, + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "modified_at": null, + "verification_url": null, + "url": null, + "failure": null + }, + "suggested_edits": { + "status": "pending", + "edits": null + } + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_suggested_edits_issues_view.json b/tests/fixtures/parity/legacy/review__get_suggested_edits_issues_view.json new file mode 100644 index 0000000..1ffd8c0 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_suggested_edits_issues_view.json @@ -0,0 +1,128 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "issues", + "status": "verifying", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "suggest_edits": true, + "max_citations": 0 + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 2, + "completed": 1, + "failed": 0 + }, + "issues": 2, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 12 + }, + "poll_after_seconds": 15, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "7c83247c", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": { + "status": "completed", + "edits": [ + { + "position": 0, + "start": 23, + "end": 29, + "text": "Berlin", + "replacement": "Paris" + } + ] + } + }, + { + "claim_index": 1, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": null, + "suggested_edits": { + "status": "pending", + "edits": null + } + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_url_review.json b/tests/fixtures/parity/legacy/review__get_url_review.json new file mode 100644 index 0000000..ca8ddf6 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_url_review.json @@ -0,0 +1,106 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T09:21:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": [ + { + "start": null, + "end": null, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__get_verifying.json b/tests/fixtures/parity/legacy/review__get_verifying.json new file mode 100644 index 0000000..a388043 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_verifying.json @@ -0,0 +1,179 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "verifying", + "outcome": null, + "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": null, + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 0, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": 15, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "processing", + "content_status": "available", + "verification_id": null, + "task_id": null, + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "modified_at": null, + "verification_url": null, + "url": null, + "failure": null + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Water boils at 100 C at sea level.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + } +} diff --git a/tests/fixtures/parity/legacy/review__idempotency_body_mismatch_422.json b/tests/fixtures/parity/legacy/review__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..fc47586 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__idempotency_body_mismatch_422.json @@ -0,0 +1,20 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch", + "errors": [ + { + "loc": [ + "header" + ], + "msg": "Idempotency-Key reused with a different request body." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/review__idempotency_conflict_409.json b/tests/fixtures/parity/legacy/review__idempotency_conflict_409.json new file mode 100644 index 0000000..8bbdff7 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__idempotency_conflict_409.json @@ -0,0 +1,13 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A review with this Idempotency-Key is still being created. Retry shortly.", + "code": "idempotency_conflict", + "review_id": null + } +} diff --git a/tests/fixtures/parity/legacy/review__idempotency_conflict_409_existing_review.json b/tests/fixtures/parity/legacy/review__idempotency_conflict_409_existing_review.json new file mode 100644 index 0000000..edef440 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__idempotency_conflict_409_existing_review.json @@ -0,0 +1,13 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A review with this Idempotency-Key is still being created. Retry shortly.", + "code": "idempotency_conflict", + "review_id": "d6b2bd72" + } +} diff --git a/tests/fixtures/parity/legacy/review__idempotent_replay_202.json b/tests/fixtures/parity/legacy/review__idempotent_replay_202.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/legacy/review__idempotent_replay_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/legacy/review__receipt_202.json b/tests/fixtures/parity/legacy/review__receipt_202.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/legacy/review__receipt_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/legacy/review__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/legacy/review__receipt_202_empty_webhook_url.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/legacy/review__receipt_202_empty_webhook_url.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/legacy/review__receipt_202_webhook_url.json b/tests/fixtures/parity/legacy/review__receipt_202_webhook_url.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/legacy/review__receipt_202_webhook_url.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/legacy/review__replay_later_202.json b/tests/fixtures/parity/legacy/review__replay_later_202.json new file mode 100644 index 0000000..6a0872b --- /dev/null +++ b/tests/fixtures/parity/legacy/review__replay_later_202.json @@ -0,0 +1,14 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Retry-After": "20", + "Location": "/api/v1/reviews/d6b2bd72", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/legacy/verify__batch_202.json b/tests/fixtures/parity/legacy/verify__batch_202.json new file mode 100644 index 0000000..7229ce4 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__batch_202.json @@ -0,0 +1,21 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "claim_text": "The Earth is round." + }, + { + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "claim_text": "Water boils at 100C at sea level." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/verify__batch_capacity_503.json b/tests/fixtures/parity/legacy/verify__batch_capacity_503.json new file mode 100644 index 0000000..342c8aa --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__batch_capacity_503.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": "", + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/verify__batch_empty_422.json b/tests/fixtures/parity/legacy/verify__batch_empty_422.json new file mode 100644 index 0000000..48a686f --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__batch_empty_422.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "claims is required and must be non-empty." + } +} diff --git a/tests/fixtures/parity/legacy/verify__batch_idempotency_conflict_409.json b/tests/fixtures/parity/legacy/verify__batch_idempotency_conflict_409.json new file mode 100644 index 0000000..0b70d8c --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__batch_idempotency_conflict_409.json @@ -0,0 +1,12 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A request with this Idempotency-Key is already in progress.", + "task_id": "7cff18d" + } +} diff --git a/tests/fixtures/parity/legacy/verify__batch_item_missing_claim_422.json b/tests/fixtures/parity/legacy/verify__batch_item_missing_claim_422.json new file mode 100644 index 0000000..ad1fc01 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__batch_item_missing_claim_422.json @@ -0,0 +1,23 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "payload", + "claims", + 1, + "text" + ], + "msg": "Field required" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/verify__batch_no_credits_402.json b/tests/fixtures/parity/legacy/verify__batch_no_credits_402.json new file mode 100644 index 0000000..5ec2c08 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__batch_no_credits_402.json @@ -0,0 +1,20 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Insufficient credits for batch.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "requested": 1, + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 10 + } +} diff --git a/tests/fixtures/parity/legacy/verify__batch_partial_202.json b/tests/fixtures/parity/legacy/verify__batch_partial_202.json new file mode 100644 index 0000000..5bfc144 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__batch_partial_202.json @@ -0,0 +1,18 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "claim_text": "First claim." + } + ], + "partial": true + } +} diff --git a/tests/fixtures/parity/legacy/verify__batch_too_many_422.json b/tests/fixtures/parity/legacy/verify__batch_too_many_422.json new file mode 100644 index 0000000..a865749 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__batch_too_many_422.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Batch size exceeds maximum of 20." + } +} diff --git a/tests/fixtures/parity/legacy/verify__blank_claim_422.json b/tests/fixtures/parity/legacy/verify__blank_claim_422.json new file mode 100644 index 0000000..1ebb88e --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__blank_claim_422.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Text is required." + } +} diff --git a/tests/fixtures/parity/legacy/verify__capacity_503.json b/tests/fixtures/parity/legacy/verify__capacity_503.json new file mode 100644 index 0000000..342c8aa --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__capacity_503.json @@ -0,0 +1,15 @@ +{ + "status": 503, + "headers": { + "Content-Type": "application/json", + "Retry-After": "", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", + "code": "capacity", + "retry_after": "", + "doc_url": "https://lenz.io/docs/errors#unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/verify__idempotency_body_mismatch_422.json b/tests/fixtures/parity/legacy/verify__idempotency_body_mismatch_422.json new file mode 100644 index 0000000..ef04562 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__idempotency_body_mismatch_422.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Idempotency-Key reused with a different request body." + } +} diff --git a/tests/fixtures/parity/legacy/verify__idempotency_conflict_409.json b/tests/fixtures/parity/legacy/verify__idempotency_conflict_409.json new file mode 100644 index 0000000..0eee4f9 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__idempotency_conflict_409.json @@ -0,0 +1,12 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "A request with this Idempotency-Key is already in progress.", + "task_id": "7cff18da2d979d6370e54a160be121ad" + } +} diff --git a/tests/fixtures/parity/legacy/verify__idempotency_key_replay.json b/tests/fixtures/parity/legacy/verify__idempotency_key_replay.json new file mode 100644 index 0000000..b6dbf5e --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__idempotency_key_replay.json @@ -0,0 +1,13 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "78ac4e9f234ea3bd" + } +} diff --git a/tests/fixtures/parity/legacy/verify__implicit_dedup_200.json b/tests/fixtures/parity/legacy/verify__implicit_dedup_200.json new file mode 100644 index 0000000..011b4b9 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__implicit_dedup_200.json @@ -0,0 +1,12 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + } +} diff --git a/tests/fixtures/parity/legacy/verify__implicit_repeat_replay.json b/tests/fixtures/parity/legacy/verify__implicit_repeat_replay.json new file mode 100644 index 0000000..b6dbf5e --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__implicit_repeat_replay.json @@ -0,0 +1,13 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "78ac4e9f234ea3bd" + } +} diff --git a/tests/fixtures/parity/legacy/verify__invalid_depth_422.json b/tests/fixtures/parity/legacy/verify__invalid_depth_422.json new file mode 100644 index 0000000..5c82768 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__invalid_depth_422.json @@ -0,0 +1,24 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "literal_error", + "loc": [ + "body", + "payload", + "depth" + ], + "msg": "Input should be 'standard' or 'low'", + "ctx": { + "expected": "'standard' or 'low'" + } + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/verify__invalid_language_422.json b/tests/fixtures/parity/legacy/verify__invalid_language_422.json new file mode 100644 index 0000000..8127478 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__invalid_language_422.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact." + } +} diff --git a/tests/fixtures/parity/legacy/verify__list_200.json b/tests/fixtures/parity/legacy/verify__list_200.json new file mode 100644 index 0000000..c51ecc1 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__list_200.json @@ -0,0 +1,45 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "language": "en" + }, + { + "verification_id": "7c83247c", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:28:00.123456+00:00", + "modified_at": null, + "language": "en" + } + ], + "total": 2, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/verify__list_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/verify__list_200_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..bcbe8b3 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__list_200_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "language": "en" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/verify__list_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/verify__list_200_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..9f2e07e --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__list_200_modified_at_same_day_hours_apart.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "modified_at": null, + "language": "en" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/verify__list_clamped_200.json b/tests/fixtures/parity/legacy/verify__list_clamped_200.json new file mode 100644 index 0000000..3d639d4 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__list_clamped_200.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 0, + "page": 1, + "page_size": 100 + } +} diff --git a/tests/fixtures/parity/legacy/verify__list_empty_200.json b/tests/fixtures/parity/legacy/verify__list_empty_200.json new file mode 100644 index 0000000..b419316 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__list_empty_200.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [], + "total": 0, + "page": 1, + "page_size": 20 + } +} diff --git a/tests/fixtures/parity/legacy/verify__list_page2_200.json b/tests/fixtures/parity/legacy/verify__list_page2_200.json new file mode 100644 index 0000000..38ebd2f --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__list_page2_200.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "items": [ + { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "domain": "Science", + "entities": [], + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "language": "en" + } + ], + "total": 3, + "page": 2, + "page_size": 2 + } +} diff --git a/tests/fixtures/parity/legacy/verify__misnamed_field_422.json b/tests/fixtures/parity/legacy/verify__misnamed_field_422.json new file mode 100644 index 0000000..98527f4 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__misnamed_field_422.json @@ -0,0 +1,26 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required" + } + ], + "hint": "Required field 'claim' is missing. Unrecognised fields: statement.", + "doc_url": "https://lenz.io/docs/errors#validation", + "unrecognized_fields": [ + "statement" + ] + } +} diff --git a/tests/fixtures/parity/legacy/verify__missing_claim_422.json b/tests/fixtures/parity/legacy/verify__missing_claim_422.json new file mode 100644 index 0000000..f862968 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__missing_claim_422.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": [ + { + "type": "missing", + "loc": [ + "body", + "payload", + "text" + ], + "msg": "Field required" + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/verify__no_credits_402.json b/tests/fixtures/parity/legacy/verify__no_credits_402.json new file mode 100644 index 0000000..32f50ac --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__no_credits_402.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "No remaining claim checks.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T09:14:00+00:00", + "credits_remaining": 0, + "cost": 10 + } +} diff --git a/tests/fixtures/parity/legacy/verify__replay_later_202.json b/tests/fixtures/parity/legacy/verify__replay_later_202.json new file mode 100644 index 0000000..b6dbf5e --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__replay_later_202.json @@ -0,0 +1,13 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "78ac4e9f234ea3bd" + } +} diff --git a/tests/fixtures/parity/legacy/verify__select_202.json b/tests/fixtures/parity/legacy/verify__select_202.json new file mode 100644 index 0000000..7229ce4 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__select_202.json @@ -0,0 +1,21 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "claim_text": "The Earth is round." + }, + { + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "claim_text": "Water boils at 100C at sea level." + } + ] + } +} diff --git a/tests/fixtures/parity/legacy/verify__select_empty_422.json b/tests/fixtures/parity/legacy/verify__select_empty_422.json new file mode 100644 index 0000000..e2c29b2 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__select_empty_422.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "texts is required and must be non-empty." + } +} diff --git a/tests/fixtures/parity/legacy/verify__select_invalid_selection_422.json b/tests/fixtures/parity/legacy/verify__select_invalid_selection_422.json new file mode 100644 index 0000000..637ab1f --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__select_invalid_selection_422.json @@ -0,0 +1,13 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Selected text was not one of the offered claims: 'The Moon is made of cheese.'", + "code": "invalid_selection", + "error": "invalid_selection" + } +} diff --git a/tests/fixtures/parity/legacy/verify__select_no_selection_pending_409.json b/tests/fixtures/parity/legacy/verify__select_no_selection_pending_409.json new file mode 100644 index 0000000..3b60350 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__select_no_selection_pending_409.json @@ -0,0 +1,13 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This task has no pending claim selection.", + "code": "no_selection_pending", + "error": "no_selection_pending" + } +} diff --git a/tests/fixtures/parity/legacy/verify__select_too_many_422.json b/tests/fixtures/parity/legacy/verify__select_too_many_422.json new file mode 100644 index 0000000..297aef4 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__select_too_many_422.json @@ -0,0 +1,11 @@ +{ + "status": 422, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Cannot select more than 20 claims." + } +} diff --git a/tests/fixtures/parity/legacy/verify__select_unknown_task_404.json b/tests/fixtures/parity/legacy/verify__select_unknown_task_404.json new file mode 100644 index 0000000..09c1e79 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__select_unknown_task_404.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Task not found." + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json new file mode 100644 index 0000000..0350ab8 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Cancelled.", + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_completed.json b/tests/fixtures/parity/legacy/verify__status_completed.json new file mode 100644 index 0000000..8e62516 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_completed.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_completed_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while.json new file mode 100644 index 0000000..8e62516 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..3f7a898 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..5a6b9fc --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/verify__status_completed_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..3f7a898 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_completed_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/verify__status_completed_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..5a6b9fc --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_completed_modified_at_same_day_hours_apart.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_failed.json b/tests/fixtures/parity/legacy/verify__status_failed.json new file mode 100644 index 0000000..29f43b1 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_failed.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Pipeline stopped at: research_empty", + "failure_reason": "research_empty", + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_failed_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_failed_after_a_while.json new file mode 100644 index 0000000..f3a344f --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_failed_after_a_while.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Pipeline stopped: conclusion_failed.", + "failure_reason": "conclusion_failed", + "failure_class": "internal", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#internal" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_failed_after_a_while_framing.json b/tests/fixtures/parity/legacy/verify__status_failed_after_a_while_framing.json new file mode 100644 index 0000000..2f2bf0c --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_failed_after_a_while_framing.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Pipeline stopped: framing_failed.", + "failure_reason": "framing_failed", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_failed_retryable.json b/tests/fixtures/parity/legacy/verify__status_failed_retryable.json new file mode 100644 index 0000000..3451f74 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_failed_retryable.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Pipeline stopped at: adjudication_failed", + "failure_reason": "adjudication_failed", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_needs_input.json b/tests/fixtures/parity/legacy/verify__status_needs_input.json new file mode 100644 index 0000000..463b486 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_needs_input.json @@ -0,0 +1,24 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "reason": "multi_claim", + "claims": [ + { + "text": "The Earth is round.", + "domain": "Science" + }, + { + "text": "Water boils at 100C at sea level.", + "domain": "Science" + } + ], + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select." + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_not_a_claim.json b/tests/fixtures/parity/legacy/verify__status_not_a_claim.json new file mode 100644 index 0000000..f787d67 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_not_a_claim.json @@ -0,0 +1,18 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Not a verifiable claim.", + "failure_reason": "not_a_claim", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "hint": "The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims." + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_not_a_claim_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_not_a_claim_after_a_while.json new file mode 100644 index 0000000..ebe10ba --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_not_a_claim_after_a_while.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Pipeline stopped: not_a_claim.", + "failure_reason": "not_a_claim", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_not_yours_404.json b/tests/fixtures/parity/legacy/verify__status_not_yours_404.json new file mode 100644 index 0000000..09c1e79 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_not_yours_404.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Task not found." + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_completed.json b/tests/fixtures/parity/legacy/verify__status_older_run_completed.json new file mode 100644 index 0000000..8e62516 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_older_run_completed.json @@ -0,0 +1,99 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "4d44dfaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:21:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json b/tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json new file mode 100644 index 0000000..0a8ff9f --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Pipeline failed.", + "failure_reason": "task_error", + "failure_class": "internal", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#internal" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json b/tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json new file mode 100644 index 0000000..29f43b1 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Pipeline stopped at: research_empty", + "failure_reason": "research_empty", + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json b/tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json new file mode 100644 index 0000000..8ff9472 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "progress": { + "step": "research", + "index": 2, + "total": 5, + "elapsed_seconds": 2, + "poll_after_seconds": 5 + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_processing.json b/tests/fixtures/parity/legacy/verify__status_processing.json new file mode 100644 index 0000000..8ff9472 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_processing.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "progress": { + "step": "research", + "index": 2, + "total": 5, + "elapsed_seconds": 2, + "poll_after_seconds": 5 + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_processing_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_processing_after_a_while.json new file mode 100644 index 0000000..077dee8 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_processing_after_a_while.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "progress": { + "step": "adjudication", + "index": 4, + "total": 5, + "elapsed_seconds": 2, + "poll_after_seconds": 5 + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_task_stuck.json b/tests/fixtures/parity/legacy/verify__status_task_stuck.json new file mode 100644 index 0000000..2504c91 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_task_stuck.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "The task was never completed and has been marked failed.", + "failure_reason": "task_stuck", + "failure_class": "upstream_unavailable", + "retryable": true, + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_unknown_404.json b/tests/fixtures/parity/legacy/verify__status_unknown_404.json new file mode 100644 index 0000000..09c1e79 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_unknown_404.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Task not found." + } +} diff --git a/tests/fixtures/parity/legacy/verify__submit_202.json b/tests/fixtures/parity/legacy/verify__submit_202.json new file mode 100644 index 0000000..b6dbf5e --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__submit_202.json @@ -0,0 +1,13 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "78ac4e9f234ea3bd" + } +} diff --git a/tests/fixtures/parity/legacy/verify__submit_202_options.json b/tests/fixtures/parity/legacy/verify__submit_202_options.json new file mode 100644 index 0000000..b6dbf5e --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__submit_202_options.json @@ -0,0 +1,13 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "78ac4e9f234ea3bd" + } +} diff --git a/tests/fixtures/parity/legacy/verify__submit_202_text_alias.json b/tests/fixtures/parity/legacy/verify__submit_202_text_alias.json new file mode 100644 index 0000000..b6dbf5e --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__submit_202_text_alias.json @@ -0,0 +1,13 @@ +{ + "status": 202, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "78ac4e9f234ea3bd" + } +} diff --git a/tests/fixtures/parity/legacy/verify__unauthenticated_401.json b/tests/fixtures/parity/legacy/verify__unauthenticated_401.json new file mode 100644 index 0000000..8dd2fb4 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__unauthenticated_401.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Unauthorized" + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_200.json b/tests/fixtures/parity/legacy/verify__verification_200.json new file mode 100644 index 0000000..b4a8eae --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_200.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_200_covered.json b/tests/fixtures/parity/legacy/verify__verification_200_covered.json new file mode 100644 index 0000000..5ff9574 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_200_covered.json @@ -0,0 +1,93 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "covered", + "reasons": [], + "certificate_id": "def7232d27eda03cf261569aee9948c5", + "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "as_of": "2026-03-10T12:01:30+00:00", + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..c57efcb --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_null.json b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_null.json new file mode 100644 index 0000000..b4a8eae --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_null.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..4480b71 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_same_day_hours_apart.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_set.json b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_set.json new file mode 100644 index 0000000..c57efcb --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_set.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_200_result_json_conclusion_only.json b/tests/fixtures/parity/legacy/verify__verification_200_result_json_conclusion_only.json new file mode 100644 index 0000000..86594fb --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_200_result_json_conclusion_only.json @@ -0,0 +1,56 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [], + "presumed_intent": "", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [], + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_pro": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "debate_con": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "panel_agreement": "split" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_200_result_json_empty.json b/tests/fixtures/parity/legacy/verify__verification_200_result_json_empty.json new file mode 100644 index 0000000..86594fb --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_200_result_json_empty.json @@ -0,0 +1,56 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [], + "presumed_intent": "", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [], + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_pro": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "debate_con": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "panel_agreement": "split" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_200_result_json_old_shape_strings.json b/tests/fixtures/parity/legacy/verify__verification_200_result_json_old_shape_strings.json new file mode 100644 index 0000000..34ae356 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_200_result_json_old_shape_strings.json @@ -0,0 +1,72 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [], + "presumed_intent": "", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "", + "title": "", + "url": "https://old.example/a", + "snippet": "Old row.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "", + "score": null, + "reasoning": "", + "warnings": [] + } + ], + "debate_pro": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "debate_con": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "panel_agreement": "split" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_404.json b/tests/fixtures/parity/legacy/verify__verification_404.json new file mode 100644 index 0000000..3bf3204 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_404.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Not found." + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_by_task_id_200.json b/tests/fixtures/parity/legacy/verify__verification_by_task_id_200.json new file mode 100644 index 0000000..b4a8eae --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_by_task_id_200.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_failed_409.json b/tests/fixtures/parity/legacy/verify__verification_failed_409.json new file mode 100644 index 0000000..fc6de0a --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_failed_409.json @@ -0,0 +1,19 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This check failed and has no result.", + "code": "verification_failed", + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "hint": "Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "failure_reason": "research_empty", + "failure_class": "insufficient_evidence", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_failed_409_not_a_claim_after_a_while.json b/tests/fixtures/parity/legacy/verify__verification_failed_409_not_a_claim_after_a_while.json new file mode 100644 index 0000000..d0c0aa6 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_failed_409_not_a_claim_after_a_while.json @@ -0,0 +1,19 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This check failed and has no result.", + "code": "verification_failed", + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "hint": "Resubmit to POST /verify if retryable is true. GET /verify/status/7cff18da2d979d6370e54a160be121ad has the full failure.", + "failure_reason": "not_a_claim", + "failure_class": "invalid_input", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#invalid-input" + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_not_ready_409.json b/tests/fixtures/parity/legacy/verify__verification_not_ready_409.json new file mode 100644 index 0000000..d5ace3f --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_not_ready_409.json @@ -0,0 +1,15 @@ +{ + "status": 409, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This check is still running.", + "code": "verification_not_ready", + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "hint": "Poll GET /verify/status/7cff18da2d979d6370e54a160be121ad until it completes, then read its result." + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_not_yours_404.json b/tests/fixtures/parity/legacy/verify__verification_not_yours_404.json new file mode 100644 index 0000000..3bf3204 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_not_yours_404.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Not found." + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_public_anonymous.json b/tests/fixtures/parity/legacy/verify__verification_public_anonymous.json new file mode 100644 index 0000000..6f459a1 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_public_anonymous.json @@ -0,0 +1,83 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "public", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": null + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_public_of_another_user.json b/tests/fixtures/parity/legacy/verify__verification_public_of_another_user.json new file mode 100644 index 0000000..b81a9a7 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_public_of_another_user.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "public", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical in shape.", + "executive_summary": "The claim is verified.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_unknown_task_404.json b/tests/fixtures/parity/legacy/verify__verification_unknown_task_404.json new file mode 100644 index 0000000..cfc00c7 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_unknown_task_404.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "Not found.", + "hint": "Unknown id. If this is a task_id, poll GET /verify/status/dddddddddddddddddddddddddddddddd for its status." + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_zero_retention_expired_410.json b/tests/fixtures/parity/legacy/verify__verification_zero_retention_expired_410.json new file mode 100644 index 0000000..010b258 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_zero_retention_expired_410.json @@ -0,0 +1,13 @@ +{ + "status": 410, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "detail": "This verification is no longer available: its account removes verifications after a set period.", + "code": "purged", + "purged_at": "2026-10-01T09:07:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_zero_retention_served.json b/tests/fixtures/parity/legacy/verify__verification_zero_retention_served.json new file mode 100644 index 0000000..21c8d25 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_zero_retention_served.json @@ -0,0 +1,95 @@ +{ + "status": 200, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + }, + "body": { + "verification_id": "68ca4aaf", + "claim": "The Earth is round.", + "visibility": "private", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "presumed_intent": "Verify a basic scientific fact", + "verdict": "True", + "confidence": "high", + "lenz_score": 9, + "key_finding": "The Earth is approximately spherical.", + "executive_summary": "The claim is well supported by evidence.", + "warnings": [ + "Relies on limited sources" + ], + "suggested_rewrite": null, + "created_at": "2026-10-01T09:14:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape", + "snippet": "Satellite imagery shows an oblate spheroid.", + "date": "2025-01-15" + }, + { + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth", + "snippet": "Orbital measurements confirm the curvature.", + "date": "2025-01-16" + } + ], + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Claim Precision & Quantitative Accuracy", + "score": 9.0, + "reasoning": "Precision analysis reasoning.", + "warnings": [] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Sources", + "score": 9.0, + "reasoning": "Source audit reasoning.", + "warnings": [ + "weak source A" + ] + } + ], + "debate_pro": { + "role": "Advocate", + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold." + }, + "debate_con": { + "role": "Skeptic", + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + } +} diff --git a/tests/fixtures/parity/legacy/webhook__certificate_timestamped.json b/tests/fixtures/parity/legacy/webhook__certificate_timestamped.json new file mode 100644 index 0000000..a47b14c --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__certificate_timestamped.json @@ -0,0 +1,27 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "certificate.timestamped", + "verification_id": "68ca4aaf", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "batch_id": null, + "status": "completed", + "result": null, + "needs_input": null, + "coverage": { + "status": "covered", + "reasons": [], + "certificate_id": "def7232d27eda03cf261569aee9948c5", + "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "as_of": "2026-09-03T10:00:00+00:00", + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + }, + "error": null, + "attempt": 1, + "delivered_at": "2026-10-01T09:28:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__certificate_timestamped_review_child.json b/tests/fixtures/parity/legacy/webhook__certificate_timestamped_review_child.json new file mode 100644 index 0000000..028efa3 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__certificate_timestamped_review_child.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "certificate.timestamped", + "verification_id": "68ca4aaf", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "batch_id": null, + "status": "completed", + "result": null, + "needs_input": null, + "coverage": { + "status": "covered", + "reasons": [], + "certificate_id": "def7232d27eda03cf261569aee9948c5", + "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "as_of": "2026-09-03T10:00:00+00:00", + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + }, + "error": null, + "review_id": "d6b2bd72", + "attempt": 1, + "delivered_at": "2026-10-01T09:28:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__citecheck_completed.json b/tests/fixtures/parity/legacy/webhook__citecheck_completed.json new file mode 100644 index 0000000..447a098 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__citecheck_completed.json @@ -0,0 +1,134 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "citecheck.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "citecheck_id": "98c0e7d5", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "status": "completed", + "citecheck": { + "citecheck_id": "98c0e7d5", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 1 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "contradicted", + "source": "support", + "is_issue": true + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "contradicted", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [ + { + "citation_index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "finding": "contradicted", + "source": "support", + "snippet": "The source says so.", + "rationale": "The page states it.", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "failure": null + } + ], + "citation_failures": [], + "failure": null, + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:42:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__citecheck_failed.json b/tests/fixtures/parity/legacy/webhook__citecheck_failed.json new file mode 100644 index 0000000..19627f2 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__citecheck_failed.json @@ -0,0 +1,47 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "citecheck.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "citecheck_id": "98c0e7d5", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "status": "failed", + "citecheck": { + "citecheck_id": "98c0e7d5", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 20 + }, + "summary": { + "citations_found": null, + "citations_selected": null, + "citation_limit": 20, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [], + "citation_issues": [], + "citation_failures": [], + "failure": { + "failure_reason": "no_citations", + "failure_class": "invalid_input", + "retryable": false, + "hint": "The text has no citation to check: no link, DOI or numbered reference with one.", + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_citations": null + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:42:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__review_completed.json b/tests/fixtures/parity/legacy/webhook__review_completed.json new file mode 100644 index 0000000..1149eea --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__review_completed.json @@ -0,0 +1,156 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "review.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "8cb2f001", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "status": "completed", + "review": { + "review_id": "8cb2f001", + "view": "full", + "status": "completed", + "outcome": "issues_found", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 1, + "failed": 0 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 11 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "verification", + "verification_id": "8ceed6f0", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", + "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "failure": null, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "verification", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "status": "completed", + "content_status": "available", + "task_id": "5ccac77f53ff49328ec134fb29d66542", + "verification_id": "8ceed6f0", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "verdict": "False", + "confidence": "high", + "lenz_score": 2, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "warnings": [ + "w1" + ], + "created_at": "2026-10-01T09:56:00.123456+00:00", + "modified_at": null, + "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", + "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "failure": null + }, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T10:03:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__review_completed_key_default_url.json b/tests/fixtures/parity/legacy/webhook__review_completed_key_default_url.json new file mode 100644 index 0000000..35c4a30 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__review_completed_key_default_url.json @@ -0,0 +1,105 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "review.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "8cb2f001", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "status": "completed", + "review": { + "review_id": "8cb2f001", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:42:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__review_completed_oversized_rebuilt.json b/tests/fixtures/parity/legacy/webhook__review_completed_oversized_rebuilt.json new file mode 100644 index 0000000..6248eaf --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__review_completed_oversized_rebuilt.json @@ -0,0 +1,11 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "review.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "8cb2f001", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "status": "completed" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__review_failed.json b/tests/fixtures/parity/legacy/webhook__review_failed.json new file mode 100644 index 0000000..294d967 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__review_failed.json @@ -0,0 +1,73 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "review.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "8cb2f001", + "task_id": "13610f0b64b63e006dc66364e0e82c85", + "status": "failed", + "review": { + "review_id": "8cb2f001", + "view": "full", + "status": "failed", + "outcome": "unchecked", + "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T09:35:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": null, + "claim_limit": 20, + "claim_limit_reached": null, + "input_truncated": false, + "assessments": null, + "verifications": null, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 0 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [], + "citations": [], + "failure": { + "failure_reason": "no_claim", + "failure_class": "invalid_input", + "retryable": false, + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "more_claims": null, + "more_claim_locations": null, + "more_citations": null + }, + "attempt": 1, + "delivered_at": "2026-10-01T09:42:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_completed.json b/tests/fixtures/parity/legacy/webhook__verification_completed.json new file mode 100644 index 0000000..68b9dbb --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_completed.json @@ -0,0 +1,107 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.completed", + "verification_id": "68ca4aaf", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "batch_id": null, + "status": "completed", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "visibility": "private", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "presumed_intent": "Check where a landmark is.", + "verdict": "False", + "confidence": "high", + "lenz_score": 1, + "key_finding": "The Eiffel Tower is in Paris.", + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "warnings": [ + "Only two sources were found." + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "created_at": "2026-10-01T09:21:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "date": "2025-01-15" + }, + { + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Evidence", + "score": 1.0, + "reasoning": "Both sources place the tower in Paris.", + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Evidence", + "score": 2.0, + "reasoning": "The encyclopedia entry is unambiguous.", + "warnings": [] + } + ], + "debate_pro": { + "role": "pro", + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable." + }, + "debate_con": { + "role": "con", + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + }, + "needs_input": null, + "coverage": null, + "error": null, + "attempt": 1, + "delivered_at": "2026-10-01T09:28:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_completed_in_batch.json b/tests/fixtures/parity/legacy/webhook__verification_completed_in_batch.json new file mode 100644 index 0000000..be55b0a --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_completed_in_batch.json @@ -0,0 +1,107 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.completed", + "verification_id": "68ca4aaf", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "batch_id": "4ee0c3aa1b671eaa", + "status": "completed", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "visibility": "private", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "presumed_intent": "Check where a landmark is.", + "verdict": "False", + "confidence": "high", + "lenz_score": 1, + "key_finding": "The Eiffel Tower is in Paris.", + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "warnings": [ + "Only two sources were found." + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "created_at": "2026-10-01T09:28:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "date": "2025-01-15" + }, + { + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Evidence", + "score": 1.0, + "reasoning": "Both sources place the tower in Paris.", + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Evidence", + "score": 2.0, + "reasoning": "The encyclopedia entry is unambiguous.", + "warnings": [] + } + ], + "debate_pro": { + "role": "pro", + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable." + }, + "debate_con": { + "role": "con", + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + }, + "needs_input": null, + "coverage": null, + "error": null, + "attempt": 1, + "delivered_at": "2026-10-01T09:35:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json new file mode 100644 index 0000000..c9af648 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json @@ -0,0 +1,107 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.completed", + "verification_id": "68ca4aaf", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "batch_id": null, + "status": "completed", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "visibility": "private", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "presumed_intent": "Check where a landmark is.", + "verdict": "False", + "confidence": "high", + "lenz_score": 1, + "key_finding": "The Eiffel Tower is in Paris.", + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "warnings": [ + "Only two sources were found." + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "created_at": "2026-10-01T23:55:00.123456+00:00", + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "sources": [ + { + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "date": "2025-01-15" + }, + { + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Evidence", + "score": 1.0, + "reasoning": "Both sources place the tower in Paris.", + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Evidence", + "score": 2.0, + "reasoning": "The encyclopedia entry is unambiguous.", + "warnings": [] + } + ], + "debate_pro": { + "role": "pro", + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable." + }, + "debate_con": { + "role": "con", + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + }, + "needs_input": null, + "coverage": null, + "error": null, + "attempt": 1, + "delivered_at": "2026-10-01T09:35:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_same_day_hours_apart.json new file mode 100644 index 0000000..6d2f10a --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_same_day_hours_apart.json @@ -0,0 +1,107 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.completed", + "verification_id": "68ca4aaf", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "batch_id": null, + "status": "completed", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "visibility": "private", + "depth": "standard", + "domain": "Geography", + "entities": [ + { + "name": "Eiffel Tower", + "qid": null + }, + { + "name": "Berlin", + "qid": null + } + ], + "presumed_intent": "Check where a landmark is.", + "verdict": "False", + "confidence": "high", + "lenz_score": 1, + "key_finding": "The Eiffel Tower is in Paris.", + "executive_summary": "The Eiffel Tower stands in Paris, France, not in Berlin.", + "warnings": [ + "Only two sources were found." + ], + "suggested_rewrite": "The Eiffel Tower is in Paris.", + "created_at": "2026-10-01T08:00:00.123456+00:00", + "modified_at": null, + "sources": [ + { + "source_name": "Example Encyclopedia", + "title": "Eiffel Tower", + "url": "https://example.org/eiffel-tower", + "snippet": "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France.", + "date": "2025-01-15" + }, + { + "source_name": "Example Travel", + "title": "Visiting Paris", + "url": "https://example.com/visit-paris", + "snippet": "Most visitors climb the tower on their first day in Paris.", + "date": "" + } + ], + "audit": { + "adjudication_summary": "Both reviewers found it false.", + "assessments": [ + { + "panelist_name": "Reviewer A", + "focus_area": "Evidence", + "score": 1.0, + "reasoning": "Both sources place the tower in Paris.", + "warnings": [ + "Example Travel is a travel blog." + ] + }, + { + "panelist_name": "Reviewer B", + "focus_area": "Evidence", + "score": 2.0, + "reasoning": "The encyclopedia entry is unambiguous.", + "warnings": [] + } + ], + "debate_pro": { + "role": "pro", + "argument": "Some guides place it in Berlin.", + "rebuttal": "None of them are reliable." + }, + "debate_con": { + "role": "con", + "argument": "Every source places it in Paris.", + "rebuttal": "The pro case cites no source." + }, + "panel_agreement": "unanimous" + }, + "language": "en", + "coverage": { + "status": "uncovered", + "reasons": [ + "plan" + ], + "certificate_id": null, + "certificate_url": null, + "as_of": null, + "currency": "EUR", + "cap": 10000, + "aggregate": 500000, + "terms_version": "v1" + } + }, + "needs_input": null, + "coverage": null, + "error": null, + "attempt": 1, + "delivered_at": "2026-10-01T09:28:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_failed_insufficient_evidence.json b/tests/fixtures/parity/legacy/webhook__verification_failed_insufficient_evidence.json new file mode 100644 index 0000000..e44f526 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_failed_insufficient_evidence.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.failed", + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "batch_id": null, + "status": "failed", + "result": null, + "needs_input": null, + "coverage": null, + "error": "research_insufficient", + "failure_class": "insufficient_evidence", + "retryable": false, + "attempt": 1, + "delivered_at": "2026-10-01T09:14:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_failed_not_a_claim.json b/tests/fixtures/parity/legacy/webhook__verification_failed_not_a_claim.json new file mode 100644 index 0000000..d27f1f1 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_failed_not_a_claim.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.failed", + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "batch_id": null, + "status": "failed", + "result": null, + "needs_input": null, + "coverage": null, + "error": "not_a_claim", + "failure_class": "invalid_input", + "retryable": false, + "attempt": 1, + "delivered_at": "2026-10-01T09:14:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_failed_upstream_unavailable.json b/tests/fixtures/parity/legacy/webhook__verification_failed_upstream_unavailable.json new file mode 100644 index 0000000..14ba36a --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_failed_upstream_unavailable.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.failed", + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "batch_id": null, + "status": "failed", + "result": null, + "needs_input": null, + "coverage": null, + "error": "framing_failed", + "failure_class": "upstream_unavailable", + "retryable": true, + "attempt": 1, + "delivered_at": "2026-10-01T09:14:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_needs_input_multi_claim.json b/tests/fixtures/parity/legacy/webhook__verification_needs_input_multi_claim.json new file mode 100644 index 0000000..4d08431 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_needs_input_multi_claim.json @@ -0,0 +1,30 @@ +{ + "status": 200, + "headers": {}, + "body": { + "event": "verification.needs_input", + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "batch_id": null, + "status": "needs_input", + "result": null, + "needs_input": { + "reason": "multi_claim", + "claims": [ + { + "text": "The Earth is round.", + "domain": "" + }, + { + "text": "The Moon is made of cheese.", + "domain": "Science" + } + ], + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select." + }, + "coverage": null, + "error": null, + "attempt": 1, + "delivered_at": "2026-10-01T09:14:00.123456+00:00" + } +} diff --git a/tests/fixtures/parity/requests.json b/tests/fixtures/parity/requests.json new file mode 100644 index 0000000..699837e --- /dev/null +++ b/tests/fixtures/parity/requests.json @@ -0,0 +1,68 @@ +{ + "citecheck_default": { + "text": "The Eiffel Tower is in Berlin. See https://example.org/eiffel." + }, + "citecheck_pairs": { + "language": "fr", + "pairs": [ + { + "statement": "The tower is in Paris.", + "url": "https://example.org/eiffel" + } + ] + }, + "citecheck_webhook_empty": { + "text": "The Eiffel Tower is in Berlin. See https://example.org/eiffel.", + "webhook_url": "" + }, + "citecheck_webhook_url": { + "max_citations": 4, + "text": "The Eiffel Tower is in Berlin. See https://example.org/eiffel.", + "webhook_url": "https://hooks.example.test/x" + }, + "review_default": { + "text": "The Eiffel Tower is in Berlin. See https://example.org/eiffel.", + "visibility": "private" + }, + "review_options": { + "escalate": { + "confidence": [ + "low" + ], + "depth": "low", + "max_assessments": 5, + "max_citations": 3, + "max_verifications": 2, + "suggest_edits": true, + "verdicts": [ + "False" + ] + }, + "language": "de", + "text": "The Eiffel Tower is in Berlin. See https://example.org/eiffel.", + "visibility": "unlisted" + }, + "review_webhook_empty": { + "text": "The Eiffel Tower is in Berlin. See https://example.org/eiffel.", + "visibility": "private", + "webhook_url": "" + }, + "review_webhook_url": { + "text": "The Eiffel Tower is in Berlin. See https://example.org/eiffel.", + "visibility": "private", + "webhook_url": "https://hooks.example.test/x" + }, + "verify_batch_webhook_url": { + "claims": [ + { + "text": "The Earth is round." + } + ], + "webhook_url": "https://hooks.example.test/x" + }, + "verify_webhook_url": { + "source_url": "", + "text": "The Earth is round.", + "webhook_url": "https://hooks.example.test/x" + } +} diff --git a/tests/parity_generate.py b/tests/parity_generate.py new file mode 100644 index 0000000..e104cb9 --- /dev/null +++ b/tests/parity_generate.py @@ -0,0 +1,37 @@ +"""Freeze what the previous release made of every original-shape response. + +Run once against the code BEFORE this SDK read the newer response shape, with +that code first on the path, e.g. from a checkout of the parent commit:: + + PYTHONPATH=/path/to/parent-checkout/src:tests python tests/parity_generate.py + +It writes ``tests/fixtures/parity/expected/.json`` and +``tests/fixtures/parity/requests.json``: the oracle +``tests/test_parity.py`` holds both response shapes to. Do not regenerate it +with the current code; that would make the test compare the code with itself. +""" + +from __future__ import annotations + +import json +import sys + +from parity_observe import FIXTURES, load, names, observe +from parity_requests import bodies + +import lenz_io + + +def main() -> None: + print(f"observing with lenz_io from {lenz_io.__file__}", file=sys.stderr) + out = FIXTURES / "expected" + out.mkdir(exist_ok=True) + for name in names(): + result = observe(name, load("legacy", name)) + (out / name).write_text(json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True) + "\n") + (FIXTURES / "requests.json").write_text(json.dumps(bodies(), indent=2, sort_keys=True) + "\n") + print(f"wrote {len(names())} files and requests.json", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/tests/parity_observe.py b/tests/parity_observe.py new file mode 100644 index 0000000..242d5e0 --- /dev/null +++ b/tests/parity_observe.py @@ -0,0 +1,233 @@ +"""What the SDK and the CLI make of one response, as plain JSON. + +Shared by ``tests/test_parity.py`` and ``tests/parity_generate.py``. It uses +only the public surface that existed before this SDK read the newer response +shape, so the generator can run it against the previous release's code and +freeze the result (``tests/fixtures/parity/expected/``): the frozen output is +the oracle both response shapes are held to. + +Fixture names are ``__.json``; the area picks what is observed. +""" + +from __future__ import annotations + +import contextlib +import io +import json +import warnings +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from rich.console import Console + +import lenz_io.cli.render as render_mod +from lenz_io import errors as errors_mod +from lenz_io import models +from lenz_io.cli import citecheck as citecheck_cli +from lenz_io.cli import review as review_cli +from lenz_io.cli.errors import friendly_text, to_payload +from lenz_io.client import Lenz +from lenz_io.webhooks import parse_webhook + +FIXTURES = Path(__file__).parent / "fixtures" / "parity" +MISSING = "" + +#: The time the CLI's relative reset dates are rendered against. +FROZEN_NOW = datetime(2026, 10, 2, 12, 0, 0, tzinfo=timezone.utc) + +#: Every attribute an SDK exception may carry, read when present. +_ERROR_ATTRS = ( + "message", + "cause", + "fix", + "doc_url", + "request_id", + "status_code", + "code", + "retry_after", + "limit", + "reset_in_seconds", + "upgrade_url", + "remaining", + "resets_at", + "requested", + "credit_balance", + "cost", + "errors", + "task_id", + "status", + "hint", + "failure_reason", + "failure_class", + "retryable", + "purged_at", + "kind", + "payload", +) + + +class _FrozenDatetime(datetime): + @classmethod + def now(cls, tz: Any = None) -> Any: # type: ignore[override] + return FROZEN_NOW if tz else FROZEN_NOW.replace(tzinfo=None) + + +def _jsonable(value: Any) -> Any: + if hasattr(value, "model_dump"): + return value.model_dump(mode="json") + return json.loads(json.dumps(value, default=str)) + + +def attr_view(obj: Any, template: Any) -> Any: + """Read ``obj`` by ATTRIBUTE along the keys of ``template`` (a dump). + + A dump shows only what a model serialises; this reads what code reading + the attributes gets, so a newer-shape response can be held to the dump the + original shape produced. + """ + if isinstance(template, dict): + out = {} + for key, sub in template.items(): + if isinstance(obj, dict): + value = obj.get(key, MISSING) + else: + value = getattr(obj, key, MISSING) + out[key] = MISSING if value is MISSING else attr_view(value, sub) + return out + if isinstance(template, list): + items = list(obj) if isinstance(obj, (list, tuple)) else [] + if len(items) != len(template): + return _jsonable(obj) + return [attr_view(o, t) for o, t in zip(items, template, strict=True)] + return _jsonable(obj) + + +def _console() -> tuple[Console, io.StringIO]: + buf = io.StringIO() + return Console(file=buf, no_color=True, width=120, highlight=False, force_terminal=False), buf + + +def _render(fn: Any, *args: Any, json_mode: bool = False, **kwargs: Any) -> str: + out = render_mod.Output(json_mode=json_mode, no_color=True) + out.json_mode = json_mode + out.console, buf = _console() + out.err, _ = _console() + stdout = io.StringIO() + real_dt = render_mod.datetime + render_mod.datetime = _FrozenDatetime # type: ignore[misc] + try: + with contextlib.redirect_stdout(stdout): + fn(out, *args, **kwargs) + except SystemExit as exc: + buf.write(f"") + finally: + render_mod.datetime = real_dt # type: ignore[misc] + return buf.getvalue() + stdout.getvalue() + + +def _error_view(err: BaseException) -> dict[str, Any]: + view: dict[str, Any] = {"type": type(err).__name__} + for name in _ERROR_ATTRS: + if name in vars(err) or hasattr(type(err), name): + view[name] = _jsonable(getattr(err, name)) + view["friendly_text"] = friendly_text(err) + view["payload_json"] = _jsonable(to_payload(err)) + return view + + +def _model_for(name: str) -> Any: + area = name.split("__", 1)[0] + case = name.split("__", 1)[1] + if area == "extract": + return models.ExtractedClaims + if area == "assess": + return models.AssessResponse + if area == "account" and case.startswith("me_usage"): + return models.Usage + if area == "account" and case.startswith("library"): + return models.LibraryList + if area == "review": + return models.ReviewStarted if case.startswith(("receipt", "idempotent", "replay")) else models.ReviewFull + if area == "citecheck": + return models.CitecheckStarted if case.startswith(("receipt", "idempotent")) else models.Citecheck + if area == "verify": + if case.startswith("status_"): + return models.TaskStatus + if case.startswith(("batch_", "select_")): + return models.BatchAccepted + if case.startswith("list"): + return models.VerificationList + if case.startswith("verification_"): + return models.Verification + return models.TaskAccepted + return None + + +def observe(name: str, fixture: dict[str, Any]) -> dict[str, Any]: + """Everything the SDK and the CLI produce from one recorded response.""" + status, body = fixture["status"], fixture["body"] + headers = {k: str(v) for k, v in (fixture.get("headers") or {}).items()} + result: dict[str, Any] = {} + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + if name.startswith("webhook__"): + event = parse_webhook(body) + view: dict[str, Any] = {"type": type(event).__name__} + for key, value in vars(event).items(): + if key != "raw": + view[key] = _jsonable(value) + result["event"] = view + return result + if status >= 400: + err = errors_mod.map_response_to_error(status, json.dumps(body).encode(), headers) + result["error"] = _error_view(err) + return result + cls = _model_for(name) + if cls is None: + return result + model = cls.model_validate(body) + result["dump"] = model.model_dump(mode="json") + result["render_json"] = None + if cls is models.ExtractedClaims: + result["render"] = _render(render_mod.render_extract, model) + result["render_json"] = _render(render_mod.render_extract, model, json_mode=True) + elif cls is models.AssessResponse: + result["render"] = _render(render_mod.render_assess, model) + result["render_json"] = _render(render_mod.render_assess, model, json_mode=True) + elif cls is models.Usage: + result["render"] = _render(render_mod.render_usage, model) + result["render_json"] = _render(render_mod.render_usage, model, json_mode=True) + elif cls is models.Verification: + result["render"] = _render(render_mod.render_verification_full, model) + result["render_concise"] = _render(render_mod.render_verification, model) + result["render_json"] = _render(render_mod.render_verification_full, model, json_mode=True) + elif cls is models.TaskStatus: + result["render"] = _render(render_mod.render_task_status, model, task_id="t") + result["render_json"] = _render(render_mod.render_task_status, model, task_id="t", json_mode=True) + client = Lenz(api_key="lenz_" + "0" * 32) + try: + verification = client._verification_from_terminal(model, "t") + result["wait"] = {"returned": verification.model_dump(mode="json")} + except errors_mod.LenzError as exc: + result["wait"] = _error_view(exc) + finally: + client.close() + if model.status == "completed" and model.result is not None: + result["render_verification"] = _render(render_mod.render_verification_full, model.result) + elif cls is models.ReviewFull: + result["render"] = _render(review_cli.render_review, model) + result["render_issues"] = _render(review_cli.render_review, model, issues_only=True) + result["exit_code"] = review_cli.exit_code_for_review(model) + elif cls is models.Citecheck: + result["render"] = _render(citecheck_cli.render_citecheck, model) + result["exit_code"] = citecheck_cli.exit_code_for_citecheck(model) + return result + + +def load(shape: str, name: str) -> dict[str, Any]: + return json.loads((FIXTURES / shape / name).read_text()) + + +def names() -> list[str]: + return sorted(p.name for p in (FIXTURES / "legacy").glob("*.json")) diff --git a/tests/parity_requests.py b/tests/parity_requests.py new file mode 100644 index 0000000..826cc9f --- /dev/null +++ b/tests/parity_requests.py @@ -0,0 +1,99 @@ +"""The request bodies the SDK sends, for ``tests/test_parity.py``. + +Like ``parity_observe``, this uses only what the previous release had, so +``parity_generate.py`` can freeze the previous release's bodies as the +oracle (``tests/fixtures/parity/requests.json``). +""" + +from __future__ import annotations + +import json +from typing import Any + +import respx + +from lenz_io.client import DEFAULT_BASE_URL, Lenz + +DRAFT = "The Eiffel Tower is in Berlin. See https://example.org/eiffel." + +#: (name, method, path, response, kwargs). The /review and /citecheck rows pin +#: ``webhook_url`` in all three states: unset, ``""`` (no webhook) and a URL. +CALLS: list[tuple[str, str, str, dict[str, Any], dict[str, Any]]] = [ + ("review_default", "review", "/review", {"review_id": "r1", "status": "queued"}, {"text": DRAFT}), + ("review_webhook_empty", "review", "/review", {"review_id": "r1"}, {"text": DRAFT, "webhook_url": ""}), + ( + "review_webhook_url", + "review", + "/review", + {"review_id": "r1"}, + {"text": DRAFT, "webhook_url": "https://hooks.example.test/x"}, + ), + ( + "review_options", + "review", + "/review", + {"review_id": "r1"}, + { + "text": DRAFT, + "verdicts": ["False"], + "confidence": ["low"], + "max_assessments": 5, + "max_verifications": 2, + "depth": "low", + "max_citations": 3, + "suggest_edits": True, + "language": "de", + "visibility": "unlisted", + }, + ), + ("citecheck_default", "citecheck", "/citecheck", {"citecheck_id": "c1"}, {"text": DRAFT}), + ("citecheck_webhook_empty", "citecheck", "/citecheck", {"citecheck_id": "c1"}, {"text": DRAFT, "webhook_url": ""}), + ( + "citecheck_webhook_url", + "citecheck", + "/citecheck", + {"citecheck_id": "c1"}, + {"text": DRAFT, "webhook_url": "https://hooks.example.test/x", "max_citations": 4}, + ), + ( + "citecheck_pairs", + "citecheck", + "/citecheck", + {"citecheck_id": "c1"}, + {"pairs": [{"statement": "The tower is in Paris.", "url": "https://example.org/eiffel"}], "language": "fr"}, + ), + ( + "verify_webhook_url", + "verify_and_submit", + "/verify", + {"task_id": "t1", "status": "queued"}, + {"claim": "The Earth is round.", "webhook_url": "https://hooks.example.test/x"}, + ), + ( + "verify_batch_webhook_url", + "verify_batch", + "/verify/batch", + {"batch_id": "b1", "items": []}, + {"claims": [{"claim": "The Earth is round."}], "webhook_url": "https://hooks.example.test/x"}, + ), +] + + +def capture(method: str, path: str, response: dict[str, Any], kwargs: dict[str, Any]) -> Any: + client = Lenz(api_key="lenz_" + "0" * 32) + try: + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + route = mock.post(path).respond(202, json=response) + if method == "verify_and_submit": + client._verify_submit(claim=kwargs["claim"], webhook_url=kwargs["webhook_url"], idempotency_key="k") + elif method == "verify_batch": + client.verify_batch(idempotency_key="k", **kwargs) + else: + getattr(client, method)(idempotency_key="k", **kwargs) + return json.loads(route.calls.last.request.content) + finally: + client.close() + + +def bodies() -> dict[str, Any]: + return {name: capture(method, path, response, kwargs) for name, method, path, response, kwargs in CALLS} diff --git a/tests/test_cli.py b/tests/test_cli.py index caa271a..1051d39 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -940,7 +940,7 @@ def test_usage_json_success(monkeypatch): assert payload["verify"]["bonus"] == 20 assert payload["assess"]["remaining"] == 5070 assert payload["extract"]["calls_today"] == 4 - # The deprecated alias is still emitted (server sends it until 2026-11-29) + # The deprecated alias is still emitted (the server keeps sending it to existing callers) # and dumping it must not warn — see tests/test_usage_models.py. assert payload["verify"]["credits"] == 20 diff --git a/tests/test_input_aliases.py b/tests/test_input_aliases.py index f1013f5..ca59dbc 100644 --- a/tests/test_input_aliases.py +++ b/tests/test_input_aliases.py @@ -52,7 +52,8 @@ def test_positional_claim_is_unchanged(self, client): with respx.mock(base_url=DEFAULT_BASE) as r: route = r.post("/verify").respond(202, json={"task_id": "tsk_1", "status": "queued"}) client.verify(CLAIM) - assert _body(route) == {"text": CLAIM, "source_url": "", "webhook_url": ""} + # No ``webhook_url`` unless one was given: omitted means the key's default. + assert _body(route) == {"text": CLAIM, "source_url": ""} class TestVerifyBatch: diff --git a/tests/test_parity.py b/tests/test_parity.py new file mode 100644 index 0000000..c5fea40 --- /dev/null +++ b/tests/test_parity.py @@ -0,0 +1,207 @@ +"""Both response shapes give existing code exactly what the previous release did. + +The oracle in ``tests/fixtures/parity/expected/`` was produced by the code +BEFORE this SDK read the newer response shape (see +``tests/parity_generate.py``), from the original-shape responses in +``tests/fixtures/parity/legacy/``. Each response has its newer-shape twin in +``tests/fixtures/parity/canonical/`` (the same answer, as the API's newer +version sends it). + +* An original-shape response must produce the frozen output byte for byte: + the model dump, every CLI rendering (text and ``--json``), the exception + ``wait`` raises, the error an HTTP error maps to, the parsed webhook. +* Its newer-shape twin must produce the same values through every attribute + the previous release had, the same CLI text, the same ``wait`` outcome and + the same error, except for the few values the newer shape does not carry, + listed one by one in ``KNOWN_GAPS`` with the reason. + +``model_dump()`` and the CLI's ``--json`` show the shape the server sent, so +they are compared for the original shape only. +""" + +from __future__ import annotations + +import json +import warnings +from typing import Any + +import pytest +from parity_observe import FIXTURES, MISSING, _model_for, attr_view, load, names, observe + +EXPECTED = FIXTURES / "expected" + +# ── What the newer shape cannot reproduce ─────────────────────────────── +# +# Each gap is allowed only where the two recorded responses themselves differ +# in the value it comes from: a rule never excuses a difference the SDK made. + +#: An exception's text fields: built from the body's ``detail``. +_MESSAGE_PATHS = ("message", "cause", "friendly_text", "payload_json.error.message") + + +def _failure(body: dict[str, Any]) -> dict[str, Any]: + failure = body.get("failure") + return failure if isinstance(failure, dict) else {} + + +def _validation_items(body: dict[str, Any]) -> Any: + detail = body.get("detail") + return detail if isinstance(detail, list) else body.get("errors", []) + + +def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, Any]) -> str | None: + """Why ``path`` may differ for ``name``, or ``None`` when it may not.""" + lb, cb = legacy["body"], canonical["body"] + head, _, rest = path.partition(".") + if head == "error": + if rest in _MESSAGE_PATHS and lb.get("detail") != cb.get("detail"): + return "the server words `detail` differently (a 422 sends a sentence, not the list)" + if rest == "code" and lb.get("code") != cb.get("code"): + return "the server sends a different (or a first) `code`" + if rest.startswith("errors") and _validation_items(lb) != _validation_items(cb): + return "the server's field errors differ" + if rest in ("hint", "fix", "friendly_text", "payload_json.error.fix") and lb.get("hint") != cb.get("hint"): + return "the server words the hint differently" + if head == "wait" or path in ("dump.error", "render"): + sentence = lb.get("error") + if isinstance(sentence, str) and sentence != _failure(cb).get("detail"): + if path in ("dump.error", "render") or rest in _MESSAGE_PATHS: + return "the failed run's sentence: the newer shape's `failure.detail` is worded differently" + if path in ("dump.hint", "wait.hint") and lb.get("hint", "") != _failure(cb).get("hint", ""): + return "the newer shape carries a hint the original did not" + if path.startswith("wait.payload.") and name.startswith("verify__status_needs_input"): + return "LenzNeedsInputError.payload is the status as sent (model_dump)" + if path == "dump.chain_id" and "chain_id" in lb and "chain_id" not in cb: + return "`chain_id` is not in the newer shape" + if path == "event.task_id" and "task_id" in lb and "task_id" not in cb: + return "review/citecheck webhooks drop `task_id`; the event reads `event_id` there" + if path.startswith("event.result.") and name.startswith("webhook__verification_completed"): + return "`result` is the dict as sent; the newer shape's carries `completed_at`" + return KNOWN_GAPS.get((name, path)) + + +#: Single gaps no rule covers, each with its reason. +KNOWN_GAPS: dict[tuple[str, str], str] = { + ("assess__list_compound_item.json", "dump.claims[0].hint"): ( + "the newer shape sends no hint on a completed row with other claims found" + ), + ("assess__list_compound_item.json", "render"): "the same hint line, printed by the CLI", + ("review__get_assessment_rows_full_fields.json", "dump.claims[0].assessment.hint"): ( + "the newer shape sends no hint on a completed quick check with other claims found" + ), + ("review__get_failed_every_assessment_failed.json", "dump.failure.hint"): "the server words the hint differently", + ("review__get_failed_every_assessment_failed.json", "render"): "the same hint, printed by the CLI", + ("review__get_failed_every_assessment_failed.json", "render_issues"): "the same hint, printed by the CLI", + ("extract__not_a_claim_beside_claims.json", "dump.status"): ( + "the newer shape answers `ready` when claims came back beside a non-claim" + ), + ("verify__list_200.json", "dump.items[1].verification_id"): "the two recordings list different verifications", + ("verify__list_200.json", "dump.items[1].created_at"): "the two recordings list different verifications", + ( + "verify__verification_200_covered.json", + "dump.coverage.certificate_id", + ): "the two recordings issued different certificates", + ( + "verify__verification_200_covered.json", + "dump.coverage.certificate_url", + ): "the two recordings issued different certificates", + ("review__get_completed_issues_all_verified.json", "dump.claims[1].verification.task_id"): ( + "the two recordings ran different tasks" + ), + ( + "review__get_completed_issues_full.json", + "dump.claims[2].verification.task_id", + ): "the two recordings ran different tasks", +} + + +def _expected(name: str) -> dict[str, Any]: + return json.loads((EXPECTED / name).read_text()) + + +def _diff(a: Any, b: Any, path: str = "") -> list[tuple[str, Any, Any]]: + if isinstance(a, dict) and isinstance(b, dict): + out: list[tuple[str, Any, Any]] = [] + for key in sorted(set(a) | set(b)): + out += _diff(a.get(key, MISSING), b.get(key, MISSING), f"{path}.{key}" if path else key) + return out + if isinstance(a, list) and isinstance(b, list) and len(a) == len(b): + out = [] + for i, (x, y) in enumerate(zip(a, b, strict=True)): + out += _diff(x, y, f"{path}[{i}]") + return out + return [] if a == b else [(path, a, b)] + + +#: Attributes this release added to the webhook events. Everything else on an +#: event must read exactly as before. +NEW_EVENT_ATTRS = {"event_id", "failure", "reason", "claims"} + + +def _old_event_attrs(observed: dict[str, Any], expected: dict[str, Any]) -> dict[str, Any]: + if "event" not in observed: + return observed + event = observed["event"] + added = set(event) - set(expected["event"]) + assert added <= NEW_EVENT_ATTRS, added + return {**observed, "event": {k: v for k, v in event.items() if k in expected["event"]}} + + +@pytest.mark.parametrize("name", names()) +def test_original_shape_is_unchanged(name: str) -> None: + expected = _expected(name) + assert _old_event_attrs(observe(name, load("legacy", name)), expected) == expected + + +def _canonical_view(name: str, expected: dict[str, Any]) -> dict[str, Any]: + fixture = load("canonical", name) + got = _old_event_attrs(observe(name, fixture), expected) + view = {k: v for k, v in got.items() if k not in ("dump", "render_json")} + if "dump" in expected: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + model = _model_for(name).model_validate(fixture["body"]) + view["dump"] = attr_view(model, expected["dump"]) + returned = expected.get("wait", {}).get("returned") + if returned is not None: + # ``wait`` returns the polled result: read it by attribute too. + view["wait"] = {"returned": attr_view(model.result, returned)} + return view + + +def _canonical_names() -> list[str]: + return [n for n in names() if (FIXTURES / "canonical" / n).exists()] + + +@pytest.mark.parametrize("name", _canonical_names()) +def test_newer_shape_reads_the_same(name: str) -> None: + expected = {k: v for k, v in _expected(name).items() if k != "render_json"} + got = _canonical_view(name, expected) + legacy, canonical = load("legacy", name), load("canonical", name) + gaps = [ + (path, old, new) for path, old, new in _diff(expected, got) if _allowed(name, path, legacy, canonical) is None + ] + assert not gaps, "\n".join(f"{path}: {old!r} -> {new!r}" for path, old, new in gaps) + + +def test_every_known_gap_is_still_a_gap() -> None: + """A listed gap that closed is stale: drop it, so the list stays exact.""" + stale = [] + for (name, path), _reason in KNOWN_GAPS.items(): + expected = {k: v for k, v in _expected(name).items() if k != "render_json"} + if path not in {p for p, _, _ in _diff(expected, _canonical_view(name, expected))}: + stale.append((name, path)) + assert not stale + + +# ── Request bodies ────────────────────────────────────────────────────────── + + +def test_request_bodies_match_the_previous_release() -> None: + """/review and /citecheck keep every body byte for byte, ``webhook_url`` + included in all three states (on those endpoints ``""`` means no webhook, + so it must still be sent). A /verify or /verify/batch call with a webhook + URL is unchanged too.""" + from parity_requests import bodies + + assert bodies() == json.loads((FIXTURES / "requests.json").read_text()) diff --git a/tests/test_read_both_shapes.py b/tests/test_read_both_shapes.py new file mode 100644 index 0000000..0b3db5f --- /dev/null +++ b/tests/test_read_both_shapes.py @@ -0,0 +1,369 @@ +"""The new attributes read the same from either response shape, and the old +ones keep their meaning. ``test_parity.py`` holds the old attributes to the +previous release's output; this file checks the new ones.""" + +from __future__ import annotations + +import json +import warnings + +import pytest +import respx +from parity_observe import load + +from lenz_io import Lenz +from lenz_io.client import DEFAULT_BASE_URL +from lenz_io.errors import LenzPipelineError, LenzRateLimitError, LenzValidationError, map_response_to_error +from lenz_io.models import ( + AssessClaim, + AssessResponse, + BatchAccepted, + ExtractedClaims, + FailureBlock, + ReviewFull, + TaskAccepted, + TaskStatus, + Usage, + Verification, + VerificationList, +) +from lenz_io.webhooks import ( + CitecheckEvent, + ReviewEvent, + VerificationCompleted, + VerificationFailed, + VerificationNeedsInput, + parse_webhook, +) + +pytestmark = pytest.mark.filterwarnings("error::DeprecationWarning") + + +def _both(name: str) -> tuple[dict, dict]: + return load("legacy", name)["body"], load("canonical", name)["body"] + + +# ── /extract ── + + +@pytest.mark.parametrize( + "name", + [ + "extract__ready_one_claim.json", + "extract__ready_several_claims.json", + "extract__locate_true.json", + "extract__locate_some_dropped.json", + "extract__url_input_located.json", + "extract__no_match_with_focus.json", + "extract__not_a_claim.json", + ], +) +def test_extract_claims_reads_the_same_from_both_shapes(name): + legacy, canonical = (ExtractedClaims.model_validate(b) for b in _both(name)) + assert [c.model_dump() for c in legacy.claims] == [c.model_dump() for c in canonical.claims] + assert legacy.status == canonical.status # "not_a_claim" from either spelling + + +def test_extract_one_claim_is_a_list_of_one(): + legacy, canonical = (ExtractedClaims.model_validate(b) for b in _both("extract__ready_one_claim.json")) + assert [c.claim for c in legacy.claims] == [c.claim for c in canonical.claims] == ["Alpha rose 5% in 2024."] + assert canonical.identified_claims == [] and canonical.claim == "Alpha rose 5% in 2024." + + +def test_extract_claims_union_keeps_the_primary_from_an_older_server(): + out = ExtractedClaims.model_validate({"status": "ready", "claim": "A.", "identified_claims": ["B.", "C."]}) + assert [c.claim for c in out.claims] == ["A.", "B.", "C."] + + +def test_extract_empty_claims_does_not_fall_back_to_old_fields(): + out = ExtractedClaims.model_validate({"status": "no_match", "claims": [], "claim": "stale"}) + assert out.claims == [] and out.claim == "stale" + + +# ── /assess ── + + +def test_assess_failed_rows_from_both_shapes(): + legacy, canonical = (AssessResponse.model_validate(b) for b in _both("assess__list_mixed_rows.json")) + for resp in (legacy, canonical): + assert resp.status == "ok" + assert [r.status for r in resp.claims] == ["completed", "failed", "failed", "failed"] + assert [r.failure.code if r.failure else None for r in resp.claims] == [ + None, + "no_checkable_claim", + "upstream_unavailable", + "framing_failed", + ] + assert resp.claims[1].failure.hint.startswith("The input is a greeting.") + # the newer shape: no verdict on a failed row, read as the original "Error"/"low" + row = canonical.claims[1] + assert (row.verdict, row.confidence, row.error_code) == ("Error", "low", "no_claim") + + +def test_assess_single_no_claim_from_both_shapes(): + legacy, canonical = (AssessResponse.model_validate(b) for b in _both("assess__single_no_claim.json")) + for resp in (legacy, canonical): + assert resp.status == "no_checkable_claim" + assert resp.failure is not None and resp.failure.code == "no_checkable_claim" + assert resp.error_code == "no_claim" and resp.error + + +def test_assess_more_claims_on_a_row_from_both_shapes(): + legacy, canonical = (AssessResponse.model_validate(b) for b in _both("assess__list_compound_item.json")) + assert legacy.claims[0].more_claims == canonical.claims[0].more_claims == ["Second claim.", "Third claim."] + assert canonical.claims[0].identified_claims == ["Second claim.", "Third claim."] + + +def test_assess_empty_more_claims_does_not_fall_back(): + row = AssessClaim.model_validate({"claim": "x", "status": "completed", "verdict": "True", "more_claims": []}) + assert row.more_claims == [] and row.identified_claims == [] + row = AssessClaim.model_validate( + {"claim": "x", "verdict": "True", "more_claims": [], "identified_claims": ["stale"]} + ) + assert row.more_claims == [] + + +@pytest.mark.parametrize( + ("rows", "status"), + [ + ([{"verdict": "Error", "error_code": "no_claim"}] * 2, "no_checkable_claim"), + ([{"verdict": "Error", "error_code": "timeout"}], "error"), + ([{"verdict": "Error", "error_code": "timeout"}, {"verdict": "True"}], "ok"), + ], +) +def test_assess_status_is_computed_for_the_original_shape(rows, status): + assert AssessResponse.model_validate({"claims": rows, "error": None}).status == status + + +# ── /verify ── + + +def test_receipt_claim_from_both_shapes(): + legacy, canonical = (BatchAccepted.model_validate(b) for b in _both("verify__batch_202.json")) + assert ( + [i.claim for i in legacy.items] == [i.claim for i in canonical.items] == [i.claim_text for i in canonical.items] + ) + assert TaskAccepted.model_validate({"task_id": "t"}).claim == "" + + +@pytest.mark.parametrize( + "name", + [ + "verify__status_failed.json", + "verify__status_failed_retryable.json", + "verify__status_not_a_claim.json", + "verify__status_cancelled_after_a_while.json", + ], +) +def test_failed_status_failure_block_from_both_shapes(name): + legacy, canonical = (TaskStatus.model_validate(b) for b in _both(name)) + for st in (legacy, canonical): + assert st.failure is not None + assert st.failure.code == _both(name)[1]["failure"]["code"] + assert st.failure.failure_class == canonical.failure_class + assert st.failure.retryable is canonical.retryable + assert st.failure.detail + assert canonical.failure_reason == legacy.failure_reason + assert canonical.error == canonical.failure.detail + + +def test_needs_input_options_from_both_shapes(): + legacy, canonical = (TaskStatus.model_validate(b) for b in _both("verify__status_needs_input.json")) + assert [c.claim for c in legacy.claims] == [c.claim for c in canonical.claims] + assert [c.text for c in canonical.claims] == [c.claim for c in canonical.claims] + + +def test_completed_at_and_the_original_modified_at_rule(): + late, early = "2026-10-01T23:55:00+00:00", "2026-10-02T00:03:00+00:00" + crossed = Verification.model_validate({"created_at": late, "completed_at": early}) + assert crossed.modified_at == early + same_day = Verification.model_validate({"created_at": "2026-10-01T08:00:00Z", "completed_at": "2026-10-01T20:00Z"}) + assert same_day.modified_at is None and same_day.completed_at == "2026-10-01T20:00Z" + # a UTC calendar day, whatever offset the strings carry + offset = Verification.model_validate( + {"created_at": "2026-10-01T22:00:00+00:00", "completed_at": "2026-10-02T01:00:00+02:00"} + ) + assert offset.modified_at is None + legacy = Verification.model_validate({"created_at": late, "modified_at": early}) + assert legacy.completed_at == early + assert Verification.model_validate({"created_at": late, "modified_at": None}).completed_at is None + + +@pytest.mark.parametrize( + "name", + [ + "verify__verification_200_modified_at_crosses_midnight_by_minutes.json", + "verify__verification_200_modified_at_same_day_hours_apart.json", + "verify__list_200_modified_at_crosses_midnight_by_minutes.json", + ], +) +def test_modified_at_from_both_shapes(name): + cls = VerificationList if "list" in name else Verification + legacy, canonical = (cls.model_validate(b) for b in _both(name)) + pick = (lambda m: m.items[0]) if cls is VerificationList else (lambda m: m) + assert pick(legacy).modified_at == pick(canonical).modified_at + + +# ── /me/usage ── + + +@pytest.mark.parametrize( + "name", + ["account__me_usage_pro_extra.json", "account__me_usage_free_partly_spent.json", "account__me_usage_free.json"], +) +def test_usage_blocks_are_computed_from_the_pool(name): + legacy, canonical = (Usage.model_validate(b) for b in _both(name)) + for cap in ("verify", "ask", "assess"): + assert getattr(legacy, cap).model_dump() == getattr(canonical, cap).model_dump() + assert canonical.quota_resets_at == canonical.credits.resets_at + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + assert canonical.credits.bonus == legacy.credits.bonus + + +# ── /review ── + + +@pytest.mark.parametrize( + "name", + [ + "review__get_more_claims.json", + "review__get_claim_limit_reached_exact.json", + "review__get_citations_limit_reached.json", + "review__get_completed_issues_full.json", + "review__get_assessment_rows_full_fields.json", + ], +) +def test_review_new_names_from_both_shapes(name): + legacy, canonical = (ReviewFull.model_validate(b) for b in _both(name)) + assert legacy.summary.claim_limit_exceeded == canonical.summary.claim_limit_exceeded + assert legacy.summary.citation_limit_exceeded == canonical.summary.citation_limit_exceeded + for a, b in zip(legacy.claims, canonical.claims, strict=True): + assert a.assessment.more_claims == b.assessment.more_claims + if a.assessment.failure is not None: + assert a.assessment.failure.code == b.assessment.failure.code + + +def test_review_failure_code_reads_the_new_spelling(): + legacy, canonical = (ReviewFull.model_validate(b) for b in _both("review__get_failed_no_claim.json")) + assert legacy.failure.code == canonical.failure.code == "no_checkable_claim" + assert canonical.failure.failure_reason == "no_claim" + assert FailureBlock.model_validate({"failure_reason": "not_a_claim"}).code == "no_checkable_claim" + + +# ── dumps show what was sent ── + + +def test_model_dump_shows_the_newer_shape_as_sent(): + canonical = load("canonical", "assess__list_mixed_rows.json")["body"] + assert AssessResponse.model_validate(canonical).model_dump(mode="json") == canonical + canonical = load("canonical", "verify__batch_202.json")["body"] + assert BatchAccepted.model_validate(canonical).model_dump(mode="json") == canonical + + +# ── webhooks ── + + +@pytest.mark.parametrize( + "name", + [ + "webhook__verification_failed_not_a_claim.json", + "webhook__verification_failed_upstream_unavailable.json", + ], +) +def test_failed_webhook_from_both_shapes(name): + legacy, canonical = (parse_webhook(b) for b in _both(name)) + assert isinstance(legacy, VerificationFailed) and isinstance(canonical, VerificationFailed) + assert legacy.error == canonical.error + assert legacy.failure.code == canonical.failure.code + assert (legacy.failure_class, legacy.retryable) == (canonical.failure_class, canonical.retryable) + assert canonical.event_id + + +def test_needs_input_webhook_from_both_shapes(): + legacy, canonical = (parse_webhook(b) for b in _both("webhook__verification_needs_input_multi_claim.json")) + assert isinstance(canonical, VerificationNeedsInput) + assert legacy.needs_input == canonical.needs_input + assert [c.claim for c in legacy.claims] == [c.claim for c in canonical.claims] + assert legacy.reason == canonical.reason == "multi_claim" + + +def test_completed_webhook_result_from_both_shapes(): + name = "webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json" + legacy, canonical = (parse_webhook(b) for b in _both(name)) + assert isinstance(canonical, VerificationCompleted) + assert legacy.verification_id == canonical.verification_id + assert legacy.result["modified_at"] == canonical.result["modified_at"] + + +@pytest.mark.parametrize("name", ["webhook__review_completed.json", "webhook__citecheck_completed.json"]) +def test_review_webhook_without_task_id_reads_event_id(name): + canonical = parse_webhook(load("canonical", name)["body"]) + assert isinstance(canonical, (ReviewEvent, CitecheckEvent)) + assert canonical.task_id == canonical.event_id != "" + + +# ── errors ── + + +def test_409_failed_run_reads_the_failure_block(): + _, canonical = _both("verify__verification_failed_409.json") + err = map_response_to_error(409, json.dumps(canonical).encode()) + assert isinstance(err, LenzPipelineError) + assert (err.failure_reason, err.failure_class, err.retryable) == ( + "research_empty", + "insufficient_evidence", + False, + ) + _, canonical = _both("verify__verification_failed_409_not_a_claim_after_a_while.json") + assert map_response_to_error(409, json.dumps(canonical).encode()).failure_reason == "not_a_claim" + + +def test_validation_errors_from_both_shapes(): + legacy, canonical = _both("errors__validation_wrong_type.json") + for body in (legacy, canonical): + err = map_response_to_error(422, json.dumps(body).encode()) + assert isinstance(err, LenzValidationError) + assert [e["msg"] for e in err.errors] == ["Input should be a valid string"] + + +def test_rate_limit_wait_from_both_shapes(): + for body in _both("errors__rate_limited_extract.json"): + err = map_response_to_error(429, json.dumps(body).encode()) + assert isinstance(err, LenzRateLimitError) + assert (err.retry_after, err.reset_in_seconds) == (900, 900) + err = map_response_to_error(429, json.dumps({"code": "review_in_flight", "retry_after": 0}).encode()) + assert (err.retry_after, err.reset_in_seconds) == (0, None) + + +def test_quota_remaining_is_derived_when_absent(): + legacy, canonical = _both("citecheck__402_no_credits.json") + assert map_response_to_error(402, json.dumps(canonical).encode()).remaining == 100 + assert map_response_to_error(402, json.dumps(legacy).encode()).remaining == 100 + + +# ── requests ── + + +def _sent(call) -> dict: + with respx.mock(base_url=DEFAULT_BASE_URL, assert_all_called=False) as mock: + verify = mock.post("/verify").respond(202, json={"task_id": "t"}) + batch = mock.post("/verify/batch").respond(202, json={"batch_id": "b", "items": []}) + client = Lenz(api_key="lenz_" + "0" * 32) + call(client) + route = verify if verify.called else batch + return json.loads(route.calls.last.request.content) + + +@pytest.mark.parametrize("webhook_url", [None, ""]) +def test_verify_never_sends_an_empty_webhook_url(webhook_url): + kwargs = {} if webhook_url is None else {"webhook_url": webhook_url} + assert "webhook_url" not in _sent(lambda c: c._verify_submit(claim="x", **kwargs)) + assert "webhook_url" not in _sent(lambda c: c.verify_batch(claims=[{"claim": "x"}], **kwargs)) + item = _sent(lambda c: c.verify_batch(claims=[{"claim": "x", "webhook_url": ""}]))["claims"][0] + assert "webhook_url" not in item + + +def test_verify_sends_a_webhook_url_that_was_set(): + body = _sent(lambda c: c.verify("x", webhook_url="https://hooks.example.test/x")) + assert body["webhook_url"] == "https://hooks.example.test/x" diff --git a/tests/test_usage_models.py b/tests/test_usage_models.py index 697ad32..b54604e 100644 --- a/tests/test_usage_models.py +++ b/tests/test_usage_models.py @@ -118,13 +118,13 @@ def test_capability_blocks_are_projections_of_the_one_pool(): def test_reading_the_credits_alias_warns_and_returns_bonus(): cap = UsageCapacity.model_validate(POOL_PAYLOAD["verify"]) assert cap.bonus == 20 - with pytest.deprecated_call(match="2026-11-29"): + with pytest.deprecated_call(match="is deprecated;"): assert cap.credits == 20 def test_dumping_keeps_the_alias_and_does_not_warn(): """Serialization must stay warning-free — a `--json` dump of usage is not - a deprecated read, and the key stays on the wire until 2026-11-29.""" + a deprecated read, and the key stays on the wire for existing callers.""" cap = UsageCapacity.model_validate(POOL_PAYLOAD["verify"]) with warnings.catch_warnings(): warnings.simplefilter("error", DeprecationWarning) @@ -144,7 +144,7 @@ def test_old_server_sending_only_credits_still_fills_bonus(): def test_server_after_the_alias_removal_still_fills_credits(): - """After 2026-11-29 the server drops `credits`; the alias keeps reading.""" + """A response without `credits` (a computed block) still reads the alias.""" cap = UsageCapacity.model_validate( {"quota_used": 13, "quota_total": 520, "quota_remaining": 507, "bonus": 20, "remaining": 507} ) @@ -173,13 +173,13 @@ def test_pre_pool_server_leaves_the_balance_empty_not_wrong(): def test_reading_the_bonus_alias_warns_and_returns_extra(): c = UsageCredits.model_validate(POOL_PAYLOAD["credits"]) assert c.extra == 200 - with pytest.deprecated_call(match="2026-11-29"): + with pytest.deprecated_call(match="is deprecated;"): assert c.bonus == 200 def test_dumping_the_pool_keeps_bonus_and_does_not_warn(): """A `--json` dump is not a deprecated read, and the key stays on the wire - until 2026-11-29.""" + for existing callers.""" c = UsageCredits.model_validate(POOL_PAYLOAD["credits"]) with warnings.catch_warnings(): warnings.simplefilter("error", DeprecationWarning) @@ -193,7 +193,7 @@ def test_a_server_sending_only_bonus_still_fills_extra(): def test_a_server_sending_only_extra_still_fills_bonus(): - """After 2026-11-29 the server sends only `extra`; `bonus` keeps reading.""" + """The newer response shape sends only `extra`; `bonus` keeps reading.""" c = UsageCredits.model_validate({"total": 300, "used": 0, "remaining": 300, "extra": 200}) assert c.extra == 200 with pytest.deprecated_call(): From 8629e5068b9a89c7d21b0a3c1c1b7cc81d56830b Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 20:08:25 +0300 Subject: [PATCH 02/67] fix: match the Node SDK: identical openapi.json text, an empty task_id on newer-shape review webhooks Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 +++- openapi.json | 8 ++++---- src/lenz_io/webhooks.py | 12 +++++------- tests/test_parity.py | 8 +++++++- tests/test_read_both_shapes.py | 4 ++-- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17abab8..ff7870a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,9 @@ on upgrade. `claim_limit_exceeded` and `citation_limit_exceeded`; `CitecheckSummary.citation_limit_exceeded`. - Webhooks: `event_id` on every event, `VerificationFailed.failure`, - `VerificationNeedsInput.reason` and `.claims`. `parse_webhook` and + `VerificationNeedsInput.reason` and `.claims`. A review or citation-check + event in the newer shape carries no `task_id` (it reads `""`; deduplicate + on `event_id`, as before). `parse_webhook` and `LenzWebhooks.parse` read the newer envelope too (`event`, `event_id`, the work's id, `status`, and the polled body under `verification` / `review` / `citecheck`). diff --git a/openapi.json b/openapi.json index 97c8a8b..925314c 100644 --- a/openapi.json +++ b/openapi.json @@ -411,7 +411,7 @@ } } }, - "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\n``credits.bonus`` is the deprecated old name of ``credits.extra``, the same\nnumber, deprecated with the fields below and kept for existing callers. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15 (#664), while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated.** ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nThey are deprecated, together with the per-block ``credits`` alias, and\nkept for existing callers. Derive them instead::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias, on the same date.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.", + "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\n``credits.bonus`` is the deprecated old name of ``credits.extra``, the same\nnumber, kept for existing callers with the fields below. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15 (#664), while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated.** ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nThey are kept for existing callers, with the per-block ``credits`` alias.\nNew code derives them instead::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias, on the same date.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.", "tags": [ "Account" ], @@ -2510,7 +2510,7 @@ "type": "object" }, "MeUsageCreditsOut": { - "description": "The account's credit balance \u2014 the one pool every capability spends.\n\nTwo buckets: the monthly allowance for the current plan (resets at\n``resets_at``) and non-expiring ``extra`` credits from grants and top-ups,\nwhich are spent only once the allowance is gone. ``remaining`` is the sum\nand is what a call is checked against.\n\n``bonus`` is a DEPRECATED alias of ``extra`` (the balance's name until\n2026-09-15), deprecated with the other deprecated fields on this endpoint and kept\nfor existing callers. It was renamed because the balance holds top-ups you paid for as\nwell as grants.", + "description": "The account's credit balance \u2014 the one pool every capability spends.\n\nTwo buckets: the monthly allowance for the current plan (resets at\n``resets_at``) and non-expiring ``extra`` credits from grants and top-ups,\nwhich are spent only once the allowance is gone. ``remaining`` is the sum\nand is what a call is checked against.\n\n``bonus`` is a DEPRECATED alias of ``extra`` (the balance's name until\n2026-09-15), kept for existing callers with the other deprecated fields on\nthis endpoint. It was renamed because the balance holds top-ups you paid for as\nwell as grants.", "properties": { "total": { "default": 0, @@ -2576,7 +2576,7 @@ "type": "object" }, "MeUsageOut": { - "description": "The calling account's credit balance, and what it can still buy.\n\n``credits`` is the balance and ``costs`` is the price list. Those two,\nplus ``cost_options``, are the whole contract for a new integration.\n\nThe ``verify`` / ``ask`` / ``assess`` blocks are DEPRECATED projections of\nthat same pool into each capability's unit, kept for clients\nalready reading them.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and carries a\nper-account daily fair-use cap instead; it rejects with 429, not 402.\n\nThe Pro plan's ``plan`` is ``pro`` since 2026-09-15; it was ``developer``\nbefore (#664). ``plan_label`` reads \"Pro\" either way.", + "description": "The calling account's credit balance, and what it can still buy.\n\n``credits`` is the balance and ``costs`` is the price list. Those two,\nplus ``cost_options``, are the whole contract for a new integration.\n\nThe ``verify`` / ``ask`` / ``assess`` blocks are DEPRECATED projections of\nthat same pool into each capability's unit, kept for\nclients already reading them.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and carries a\nper-account daily fair-use cap instead; it rejects with 429, not 402.\n\nThe Pro plan's ``plan`` is ``pro`` since 2026-09-15; it was ``developer``\nbefore (#664). ``plan_label`` reads \"Pro\" either way.", "properties": { "plan": { "default": "free", @@ -2653,7 +2653,7 @@ "type": "object" }, "MeUsageQuotaOut": { - "description": "DEPRECATED. One capability's share of the pool, projected into its unit.\n\n**Deprecated**, with the per-block ``credits`` alias below, and kept for\nexisting callers. Read\n``MeUsageOut.credits`` and ``MeUsageOut.costs`` instead and do the\ndivision \u2014 it is one line, and it is the same one this block does::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nThese are **projections, not allowances**. Every billable capability draws\non the single balance in ``MeUsageOut.credits``, at the weight in\n``MeUsageOut.costs``; this block answers \"how many ``/verify`` calls could\nI still make if I spent everything on them\". Spending on any capability\nmoves every block, and the division floors \u2014 a ``verify`` block whose\nweight is 10 ticks once per 10 credits spent anywhere.\n\nThey predate the pool, when each really was its own allowance, and that is\nthe reason to retire them: two capabilities at the same price emit\nbyte-identical objects (``ask`` and ``assess`` are both 1 credit today),\nso the shape suggests a per-capability allowance that no longer exists.\n\n``bonus`` is ``MeUsageOut.credits.extra``, the non-expiring balance, in\nthis capability's unit.\n``quota_used`` is derived as ``quota_total - quota_remaining``, so\n``used + remaining == total`` always holds.\n\n``credits`` is a DEPRECATED alias of ``bonus``, kept for compatibility\nwith existing callers. It never meant the pool \u2014 before the pool existed it\nmeant this capability's one-off top-up balance, which is exactly what\n``bonus`` now reports.", + "description": "DEPRECATED. One capability's share of the pool, projected into its unit.\n\nKept for existing callers, with the per-block ``credits`` alias below. Read\n``MeUsageOut.credits`` and ``MeUsageOut.costs`` instead and do the\ndivision \u2014 it is one line, and it is the same one this block does::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nThese are **projections, not allowances**. Every billable capability draws\non the single balance in ``MeUsageOut.credits``, at the weight in\n``MeUsageOut.costs``; this block answers \"how many ``/verify`` calls could\nI still make if I spent everything on them\". Spending on any capability\nmoves every block, and the division floors \u2014 a ``verify`` block whose\nweight is 10 ticks once per 10 credits spent anywhere.\n\nThey predate the pool, when each really was its own allowance, and that is\nthe reason to retire them: two capabilities at the same price emit\nbyte-identical objects (``ask`` and ``assess`` are both 1 credit today),\nso the shape suggests a per-capability allowance that no longer exists.\n\n``bonus`` is ``MeUsageOut.credits.extra``, the non-expiring balance, in\nthis capability's unit.\n``quota_used`` is derived as ``quota_total - quota_remaining``, so\n``used + remaining == total`` always holds.\n\n``credits`` is a DEPRECATED alias of ``bonus``, kept for compatibility\nwith existing callers. It never meant the pool \u2014 before the pool existed it\nmeant this capability's one-off top-up balance, which is exactly what\n``bonus`` now reports.", "properties": { "quota_used": { "default": 0, diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index c88741b..a28409b 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -212,9 +212,9 @@ class ReviewEvent(WebhookEvent): Deduplicate on ``event_id``: it is the same on every delivery attempt of one event, while ``attempt`` counts up. ``task_id`` (deprecated) - identifies the delivery and cannot be polled on ``/verify/status``; a - payload that does not carry it reads ``event_id`` there. The deep checks a - review runs send no ``verification.*`` events of their own. + identifies the delivery and cannot be polled on ``/verify/status``; it is + ``""`` on a payload that does not carry it (the newer shape). The deep + checks a review runs send no ``verification.*`` events of their own. """ event_id: str = "" @@ -231,8 +231,8 @@ class CitecheckEvent(WebhookEvent): returns), or ``None`` if it could not be parsed (``raw["citecheck"]`` still has it). Deduplicate on ``event_id``: it is the same on every delivery attempt of one event. ``task_id`` (deprecated) identifies the - delivery and cannot be polled on ``/verify/status``; a payload that does - not carry it reads ``event_id`` there. + delivery and cannot be polled on ``/verify/status``; it is ``""`` on a + payload that does not carry it (the newer shape). """ event_id: str = "" @@ -340,7 +340,6 @@ def _build_event(payload: dict[str, Any]) -> WebhookEvent: review = ReviewFull.model_validate(review_body) if isinstance(review_body, dict) else None except ValueError: review = None - common["task_id"] = task_id or event_id return ReviewEvent( **common, review_id=str(payload.get("review_id") or _dict(review_body).get("review_id") or ""), @@ -352,7 +351,6 @@ def _build_event(payload: dict[str, Any]) -> WebhookEvent: check = Citecheck.model_validate(check_body) if isinstance(check_body, dict) else None except ValueError: check = None - common["task_id"] = task_id or event_id return CitecheckEvent( **common, citecheck_id=str(payload.get("citecheck_id") or _dict(check_body).get("citecheck_id") or ""), diff --git a/tests/test_parity.py b/tests/test_parity.py index c5fea40..90a942e 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -34,6 +34,12 @@ # # Each gap is allowed only where the two recorded responses themselves differ # in the value it comes from: a rule never excuses a difference the SDK made. +# The categories are the ones the Node SDK allows, so the two behave alike: +# the server's sentences (error / failure text), the codes and field errors it +# sends on a 422, `chain_id`, the review / citation-check webhook `task_id`, +# and a row hint the newer shape does not carry. The single entries in +# ``KNOWN_GAPS`` below fall in those categories too, plus two where the two +# recordings simply hold different ids. #: An exception's text fields: built from the body's ``detail``. _MESSAGE_PATHS = ("message", "cause", "friendly_text", "payload_json.error.message") @@ -74,7 +80,7 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, if path == "dump.chain_id" and "chain_id" in lb and "chain_id" not in cb: return "`chain_id` is not in the newer shape" if path == "event.task_id" and "task_id" in lb and "task_id" not in cb: - return "review/citecheck webhooks drop `task_id`; the event reads `event_id` there" + return "review/citecheck webhooks drop `task_id` (never pollable); dedupe on `event_id`" if path.startswith("event.result.") and name.startswith("webhook__verification_completed"): return "`result` is the dict as sent; the newer shape's carries `completed_at`" return KNOWN_GAPS.get((name, path)) diff --git a/tests/test_read_both_shapes.py b/tests/test_read_both_shapes.py index 0b3db5f..83eee5c 100644 --- a/tests/test_read_both_shapes.py +++ b/tests/test_read_both_shapes.py @@ -297,10 +297,10 @@ def test_completed_webhook_result_from_both_shapes(): @pytest.mark.parametrize("name", ["webhook__review_completed.json", "webhook__citecheck_completed.json"]) -def test_review_webhook_without_task_id_reads_event_id(name): +def test_review_webhook_without_task_id(name): canonical = parse_webhook(load("canonical", name)["body"]) assert isinstance(canonical, (ReviewEvent, CitecheckEvent)) - assert canonical.task_id == canonical.event_id != "" + assert canonical.task_id == "" and canonical.event_id != "" # ── errors ── From 36e40fad0cb94c2356280e10ca9b7da64b7cf8c0 Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 20:09:31 +0300 Subject: [PATCH 03/67] docs(changelog): drop the wrong note about idempotency keys from an earlier release Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff7870a..7770c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,9 +67,7 @@ on upgrade. empty per-item `webhook_url`). The API has always read an empty value there as "use the key's default webhook", the same as leaving it out, so nothing changes now; leaving it out keeps that meaning on later API versions, where - `""` means "no webhook". A retry that reuses an idempotency key from an - earlier release's call sends a different body and is refused as a - different request. `review` and `citecheck` send `webhook_url` exactly as + `""` means "no webhook". `review` and `citecheck` send `webhook_url` exactly as before (there `""` means "no webhook"). - `model_dump()` returns the shape the server sent: attributes filled in from the other shape are left out, so an original-shape response dumps exactly as From 7004e8c1469f303f8b1937a2ea916acc760edf48 Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 20:25:40 +0300 Subject: [PATCH 04/67] fix: parse original-shape bodies exactly as 2.20 did; newer names are properties A body is read as the newer response shape only when it carries a key only that shape has; every other body goes through the same fields, values, dump, repr, exclude_unset, JSON schema and pickle as before. The newer names (claims, status, failure, more_claims, completed_at, claim, code, detail, the *_exceeded flags, webhook event_id / failure / claims / reason) are read-only properties, not fields, and the model-level serializer is gone. Errors read the newer failure block, retry_after and a computed remaining only for a newer-shape body and only where the original key is absent. Webhook events keep their dataclass fields; a needs_input option with a null domain no longer raises. The CLI renderers are back to 2.20's code. Parity fixtures are rebuilt from the real recorded pairs (responses and webhooks), extract goes through the client with the recorded locate, and the oracle now also freezes exclude_unset dumps, repr, JSON schemas and pickles. The remaining removal-date text is gone, and the changelog lists what to settle before the SDK asks for the newer shape. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 40 +- README.md | 5 +- openapi.json | 2 +- src/lenz_io/cli/render.py | 30 +- src/lenz_io/cli/verify.py | 8 +- src/lenz_io/client.py | 30 +- src/lenz_io/errors.py | 42 +- src/lenz_io/models.py | 980 +- src/lenz_io/webhooks.py | 313 +- .../parity/canonical/account__api_root.json | 43 + .../account__ask_history_after_reset.json | 14 + .../account__ask_history_after_send.json | 25 + .../canonical/account__ask_history_empty.json | 14 + .../account__ask_history_messages.json | 25 + .../parity/canonical/account__ask_reset.json | 11 + .../parity/canonical/account__ask_send.json | 13 + .../canonical/account__ask_send_language.json | 13 + .../canonical/account__library_empty.json | 10 +- ...dified_at_crosses_midnight_by_minutes.json | 10 +- ...item_modified_at_same_day_hours_apart.json | 10 +- .../canonical/account__library_items.json | 26 +- .../canonical/account__library_no_match.json | 10 +- .../account__library_page_past_end.json | 10 +- ...ccount__library_paginated_second_page.json | 20 +- .../account__me_usage_extra_credits.json | 12 +- .../account__me_usage_extra_only.json | 12 +- .../account__me_usage_extract_calls.json | 12 +- .../canonical/account__me_usage_free.json | 12 +- .../account__me_usage_free_partly_spent.json | 12 +- .../canonical/account__me_usage_oauth.json | 40 + .../canonical/account__me_usage_plus.json | 12 +- .../canonical/account__me_usage_pro.json | 12 +- .../account__me_usage_pro_extra.json | 12 +- .../account__me_usage_stale_quota_row.json | 12 +- .../account__oauth_register_success.json | 25 + .../canonical/account__oauth_revoke.json | 9 + .../account__oauth_revoke_token.json | 9 + .../account__webhook_secret_api_key.json | 12 + ...nt__webhook_secret_insufficient_scope.json | 14 + .../account__webhook_secret_oauth.json | 11 + .../parity/canonical/assess__401_no_key.json | 8 +- .../canonical/assess__402_no_credits.json | 12 +- .../canonical/assess__422_blank_item.json | 10 +- .../canonical/assess__422_blank_text.json | 10 +- .../canonical/assess__422_input_conflict.json | 10 +- .../canonical/assess__422_item_too_long.json | 10 +- .../canonical/assess__422_no_input_field.json | 10 +- .../canonical/assess__422_too_many_items.json | 10 +- .../assess__422_wrong_type_claim.json | 10 +- .../assess__422_wrong_type_claims.json | 10 +- .../canonical/assess__502_framing_failed.json | 8 +- .../canonical/assess__503_capacity.json | 12 +- ...ss__503_list_all_upstream_unavailable.json | 12 +- .../assess__503_upstream_unavailable.json | 12 +- .../canonical/assess__deep_tier_hit.json | 10 +- .../assess__idempotency_body_mismatch.json | 8 +- .../canonical/assess__idempotency_first.json | 10 +- .../canonical/assess__idempotency_replay.json | 10 +- .../assess__list_all_error_rows.json | 10 +- .../canonical/assess__list_compound_item.json | 10 +- .../canonical/assess__list_mixed_rows.json | 10 +- .../parity/canonical/assess__memo_hit.json | 10 +- .../canonical/assess__replay_later_200.json | 10 +- .../canonical/assess__single_no_claim.json | 10 +- .../canonical/assess__single_one_claim.json | 10 +- ...ss__single_text_over_wave_more_claims.json | 10 +- .../assess__single_text_several_claims.json | 10 +- .../canonical/assess__suggest_rewrite.json | 10 +- .../canonical/citecheck__402_no_credits.json | 12 +- .../canonical/citecheck__422_empty_body.json | 10 +- .../canonical/citecheck__422_invalid_doi.json | 10 +- .../citecheck__422_max_citations_over_20.json | 10 +- ...techeck__422_max_citations_with_pairs.json | 10 +- .../citecheck__422_pair_bad_url_scheme.json | 10 +- .../citecheck__422_pair_no_source.json | 10 +- .../citecheck__422_pair_url_and_doi.json | 10 +- .../canonical/citecheck__422_pairs_empty.json | 10 +- .../citecheck__422_pairs_over_20.json | 10 +- ...citecheck__422_quote_not_in_statement.json | 10 +- .../citecheck__422_text_and_pairs.json | 10 +- .../citecheck__422_unknown_field.json | 10 +- .../canonical/citecheck__422_url_input.json | 10 +- ...citecheck__422_webhook_secret_missing.json | 22 + .../citecheck__429_citecheck_in_flight.json | 10 +- .../canonical/citecheck__503_capacity.json | 12 +- .../citecheck__503_citations_unavailable.json | 12 +- .../citecheck__get_404_not_found.json | 8 +- .../citecheck__get_404_review_id.json | 8 +- .../canonical/citecheck__get_410_purged.json | 10 +- .../canonical/citecheck__get_checking.json | 12 +- .../citecheck__get_completed_clean.json | 14 +- .../citecheck__get_completed_doi_pair.json | 14 +- .../citecheck__get_completed_incomplete.json | 14 +- ...citecheck__get_completed_issues_found.json | 14 +- ...check__get_completed_partly_supported.json | 14 +- .../citecheck__get_completed_unchecked.json | 14 +- .../citecheck__get_failed_no_citations.json | 14 +- ...heck__get_failed_upstream_unavailable.json | 14 +- .../canonical/citecheck__get_queued.json | 12 +- ...heck__get_text_limit_exactly_at_limit.json | 14 +- .../citecheck__get_text_limit_reached.json | 14 +- ...echeck__idempotency_body_mismatch_422.json | 8 +- .../citecheck__idempotency_conflict_409.json | 10 +- .../citecheck__idempotent_replay_202.json | 8 +- .../canonical/citecheck__receipt_202.json | 8 +- ...echeck__receipt_202_empty_webhook_url.json | 8 +- .../canonical/errors__ask_bad_language.json | 10 +- .../canonical/errors__ask_empty_message.json | 10 +- .../parity/canonical/errors__ask_failed.json | 8 +- .../errors__ask_idempotency_key_reused.json | 8 +- .../errors__ask_message_too_long.json | 10 +- .../canonical/errors__ask_not_completed.json | 8 +- .../errors__ask_reset_not_found.json | 8 +- .../canonical/errors__ask_send_not_found.json | 8 +- .../canonical/errors__auth_bad_key.json | 8 +- .../errors__auth_bad_oauth_token.json | 13 + .../canonical/errors__auth_basic_scheme.json | 8 +- .../errors__auth_expired_oauth_token.json | 13 + .../errors__auth_insufficient_scope.json | 10 +- .../errors__auth_missing_bearer.json | 8 +- .../errors__auth_missing_bearer_post.json | 8 +- .../errors__auth_oauth_issuer_off.json | 12 + .../canonical/errors__auth_oauth_only.json | 12 + ...rrors__auth_private_claim_ask_history.json | 8 +- .../errors__auth_private_claim_ask_send.json | 8 +- .../errors__auth_unrecognised_prefix.json | 8 +- ...rrors__http_error_library_bad_curated.json | 10 +- ...rrors__http_error_library_bad_verdict.json | 10 +- ...errors__method_not_allowed_get_verify.json | 12 + ...rors__method_not_allowed_post_library.json | 12 + .../canonical/errors__not_found_ask.json | 8 +- .../canonical/errors__not_found_route.json | 8 +- .../errors__not_found_verification.json | 8 +- .../errors__not_found_verify_status.json | 8 +- .../errors__oauth_register_disabled.json | 11 + ...auth_register_invalid_client_metadata.json | 12 + ...__oauth_register_invalid_redirect_uri.json | 12 + .../errors__oauth_revoke_invalid_client.json | 13 + .../errors__oauth_token_invalid_client.json | 13 + .../errors__oauth_token_invalid_grant.json | 12 + ...rrors__oauth_token_missing_grant_type.json | 11 + ...rs__oauth_token_refresh_invalid_grant.json | 12 + ...s__oauth_token_unsupported_grant_type.json | 11 + .../errors__payment_required_ask.json | 12 +- .../errors__payment_required_assess.json | 12 +- .../errors__payment_required_verify.json | 12 +- ...__payment_required_verify_batch_short.json | 12 +- .../errors__rate_limited_extract.json | 12 +- .../errors__service_unavailable_ask.json | 12 +- .../errors__service_unavailable_capacity.json | 12 +- .../errors__validation_malformed_json.json | 8 +- ...errors__validation_missing_field_hint.json | 10 +- ...errors__validation_unknown_field_only.json | 10 +- ...__validation_unknown_field_wrong_type.json | 10 +- .../errors__validation_wrong_type.json | 10 +- .../errors__web_assess_payment_required.json | 19 + .../errors__web_assess_signup_required.json | 14 + .../errors__web_review_rate_limited.json | 17 + .../parity/canonical/extract__401_no_key.json | 8 +- .../canonical/extract__422_blank_text.json | 10 +- .../extract__422_focus_too_long.json | 10 +- .../canonical/extract__422_missing_text.json | 10 +- .../canonical/extract__429_daily_limit.json | 12 +- .../extract__502_extraction_failed.json | 8 +- .../extract__503_upstream_unavailable.json | 12 +- .../canonical/extract__focus_ready.json | 10 +- ...xtract__idempotency_body_mismatch_422.json | 8 +- .../extract__idempotency_replay.json | 10 +- .../extract__locate_all_dropped.json | 10 +- .../canonical/extract__locate_failed.json | 10 +- .../extract__locate_leading_whitespace.json | 10 +- .../extract__locate_some_dropped.json | 10 +- .../canonical/extract__locate_true.json | 10 +- .../extract__no_match_with_focus.json | 10 +- .../canonical/extract__not_a_claim.json | 10 +- .../extract__not_a_claim_beside_claims.json | 10 +- .../canonical/extract__ready_one_claim.json | 10 +- .../extract__ready_several_claims.json | 10 +- .../canonical/extract__replay_later_200.json | 10 +- .../extract__url_fetch_502_unreadable.json | 8 +- .../extract__url_fetch_503_upstream.json | 12 +- .../canonical/extract__url_input_located.json | 10 +- .../canonical/review__401_no_credentials.json | 8 +- .../canonical/review__402_no_credits.json | 12 +- .../review__402_no_credits_exhausted.json | 12 +- .../canonical/review__422_blank_text.json | 10 +- .../review__422_citations_options_object.json | 10 +- .../canonical/review__422_depth_unknown.json | 10 +- .../review__422_invalid_confidence_band.json | 10 +- .../review__422_invalid_verdict_label.json | 10 +- .../review__422_max_assessments_negative.json | 10 +- .../review__422_max_citations_negative.json | 10 +- .../review__422_max_citations_too_big.json | 10 +- ...review__422_max_verifications_too_big.json | 10 +- .../canonical/review__422_text_missing.json | 10 +- .../review__422_text_wrong_type.json | 10 +- .../review__422_unknown_escalate_field.json | 10 +- .../review__422_unknown_top_level_field.json | 10 +- .../review__422_unsupported_language.json | 10 +- .../review__422_visibility_public.json | 10 +- .../review__422_webhook_secret_missing.json | 22 + .../review__429_extract_daily_limit.json | 12 +- .../review__429_review_in_flight.json | 10 +- .../canonical/review__503_capacity.json | 12 +- .../review__503_citations_capacity.json | 12 +- .../canonical/review__delete_not_a_route.json | 12 + .../review__get_403_insufficient_scope.json | 10 +- .../canonical/review__get_404_not_found.json | 8 +- .../review__get_404_other_account.json | 8 +- .../canonical/review__get_410_purged.json | 10 +- .../review__get_422_unknown_view.json | 10 +- .../canonical/review__get_assessing.json | 14 +- ...view__get_assessment_rows_full_fields.json | 14 +- .../review__get_citations_clean.json | 14 +- ...w__get_citations_failed_and_unchecked.json | 14 +- .../review__get_citations_issue_full.json | 14 +- ...view__get_citations_issue_issues_view.json | 14 +- ..._get_citations_limit_exactly_at_limit.json | 14 +- .../review__get_citations_limit_reached.json | 14 +- ..._get_citations_no_claim_but_citations.json | 14 +- ...w__get_citations_no_claim_no_citation.json | 14 +- .../review__get_citations_not_asked.json | 14 +- .../canonical/review__get_citations_only.json | 14 +- ...eview__get_citations_partly_supported.json | 14 +- .../review__get_citations_running.json | 12 +- ...w__get_citations_skipped_switched_off.json | 14 +- ...view__get_citations_skipped_url_input.json | 14 +- .../review__get_claim_limit_not_reached.json | 14 +- ...review__get_claim_limit_reached_exact.json | 14 +- .../review__get_completed_assess_only.json | 14 +- .../review__get_completed_clean.json | 14 +- ...ew__get_completed_issues_all_verified.json | 50 +- .../review__get_completed_issues_full.json | 34 +- ...iew__get_completed_issues_view_issues.json | 20 +- .../review__get_completed_positions.json | 14 +- ...review__get_completed_recovered_claim.json | 14 +- ...ew__get_completed_with_untraced_claim.json | 14 +- .../review__get_during_quick_check.json | 12 +- ...w__get_failed_every_assessment_failed.json | 14 +- .../review__get_failed_no_claim.json | 14 +- .../review__get_failed_no_claim_untraced.json | 14 +- .../canonical/review__get_more_claims.json | 14 +- ...dified_at_crosses_midnight_by_minutes.json | 28 +- ...tion_modified_at_same_day_hours_apart.json | 28 +- .../review__get_policy_resolved.json | 14 +- .../parity/canonical/review__get_queued.json | 12 +- .../review__get_suggested_edits.json | 30 +- ...view__get_suggested_edits_issues_view.json | 18 +- .../canonical/review__get_url_review.json | 14 +- .../canonical/review__get_verifying.json | 12 +- ...review__idempotency_body_mismatch_422.json | 8 +- .../review__idempotency_conflict_409.json | 10 +- ...mpotency_conflict_409_existing_review.json | 10 +- .../review__idempotent_replay_202.json | 8 +- .../parity/canonical/review__receipt_202.json | 8 +- ...review__receipt_202_empty_webhook_url.json | 8 +- .../review__receipt_202_webhook_url.json | 8 +- .../canonical/review__replay_later_202.json | 8 +- .../parity/canonical/verify__batch_202.json | 14 +- .../canonical/verify__batch_capacity_503.json | 12 +- .../canonical/verify__batch_empty_422.json | 10 +- ...erify__batch_idempotency_conflict_409.json | 10 +- .../verify__batch_item_missing_claim_422.json | 10 +- .../verify__batch_no_credits_402.json | 12 +- .../canonical/verify__batch_partial_202.json | 12 +- .../canonical/verify__batch_too_many_422.json | 10 +- .../canonical/verify__blank_claim_422.json | 10 +- .../canonical/verify__capacity_503.json | 12 +- .../parity/canonical/verify__delete_200.json | 11 + .../parity/canonical/verify__delete_404.json | 12 + .../verify__delete_not_yours_404.json | 12 + ...verify__idempotency_body_mismatch_422.json | 8 +- .../verify__idempotency_conflict_409.json | 10 +- .../verify__idempotency_key_replay.json | 8 +- .../canonical/verify__implicit_dedup_200.json | 8 +- .../verify__implicit_repeat_replay.json | 8 +- .../canonical/verify__invalid_depth_422.json | 10 +- .../verify__invalid_language_422.json | 10 +- .../parity/canonical/verify__list_200.json | 20 +- ...dified_at_crosses_midnight_by_minutes.json | 10 +- ..._200_modified_at_same_day_hours_apart.json | 10 +- .../canonical/verify__list_clamped_200.json | 10 +- .../canonical/verify__list_empty_200.json | 10 +- .../canonical/verify__list_page2_200.json | 14 +- .../canonical/verify__misnamed_field_422.json | 10 +- .../canonical/verify__missing_claim_422.json | 10 +- .../canonical/verify__no_credits_402.json | 12 +- .../canonical/verify__replay_later_202.json | 10 +- .../parity/canonical/verify__select_202.json | 14 +- .../canonical/verify__select_empty_422.json | 10 +- .../verify__select_invalid_selection_422.json | 10 +- ...rify__select_no_selection_pending_409.json | 8 +- .../verify__select_too_many_422.json | 10 +- .../verify__select_unknown_task_404.json | 8 +- ...erify__status_cancelled_after_a_while.json | 10 +- .../canonical/verify__status_completed.json | 16 +- ...erify__status_completed_after_a_while.json | 16 +- ...dified_at_crosses_midnight_by_minutes.json | 12 +- ...hile_modified_at_same_day_hours_apart.json | 12 +- ...dified_at_crosses_midnight_by_minutes.json | 12 +- ...eted_modified_at_same_day_hours_apart.json | 12 +- .../canonical/verify__status_failed.json | 10 +- .../verify__status_failed_after_a_while.json | 10 +- ...__status_failed_after_a_while_framing.json | 10 +- .../verify__status_failed_retryable.json | 10 +- .../canonical/verify__status_needs_input.json | 10 +- .../canonical/verify__status_not_a_claim.json | 10 +- ...ify__status_not_a_claim_after_a_while.json | 10 +- .../verify__status_not_yours_404.json | 8 +- .../verify__status_older_run_completed.json | 16 +- ...rify__status_older_run_failed_crashed.json | 10 +- ...lder_run_failed_insufficient_evidence.json | 10 +- .../verify__status_older_run_in_progress.json | 12 +- .../canonical/verify__status_processing.json | 12 +- ...rify__status_processing_after_a_while.json | 12 +- .../canonical/verify__status_task_stuck.json | 10 +- .../canonical/verify__status_unknown_404.json | 8 +- .../parity/canonical/verify__submit_202.json | 8 +- .../canonical/verify__submit_202_options.json | 8 +- .../verify__submit_202_text_alias.json | 8 +- .../verify__unauthenticated_401.json | 8 +- .../canonical/verify__verification_200.json | 14 +- .../verify__verification_200_covered.json | 18 +- ...dified_at_crosses_midnight_by_minutes.json | 10 +- ...fy__verification_200_modified_at_null.json | 14 +- ..._200_modified_at_same_day_hours_apart.json | 10 +- ...ify__verification_200_modified_at_set.json | 10 +- ...ation_200_result_json_conclusion_only.json | 14 +- ...y__verification_200_result_json_empty.json | 14 +- ...ion_200_result_json_old_shape_strings.json | 14 +- .../canonical/verify__verification_404.json | 8 +- .../verify__verification_by_task_id_200.json | 14 +- .../verify__verification_failed_409.json | 10 +- ..._failed_409_not_a_claim_after_a_while.json | 10 +- .../verify__verification_not_ready_409.json | 10 +- .../verify__verification_not_yours_404.json | 8 +- ...verify__verification_public_anonymous.json | 14 +- ...__verification_public_of_another_user.json | 14 +- ...verify__verification_unknown_task_404.json | 10 +- ...rification_zero_retention_expired_410.json | 10 +- ...y__verification_zero_retention_served.json | 14 +- .../verify__webhook_secret_missing_422.json | 22 + .../webhook__certificate_timestamped.json | 15 +- ..._certificate_timestamped_review_child.json | 17 +- .../webhook__citecheck_completed.json | 16 +- .../canonical/webhook__citecheck_failed.json | 21 +- .../canonical/webhook__review_completed.json | 43 +- ...ook__review_completed_key_default_url.json | 25 +- ...k__review_completed_oversized_rebuilt.json | 6 +- .../canonical/webhook__review_failed.json | 24 +- .../parity/canonical/webhook__test_fire.json | 12 + .../webhook__verification_completed.json | 19 +- ...hook__verification_completed_in_batch.json | 20 +- ...dified_at_crosses_midnight_by_minutes.json | 17 +- ...eted_modified_at_same_day_hours_apart.json | 17 +- ..._verification_completed_sparse_result.json | 22 + ...bhook__verification_failed_error_none.json | 24 + ...fication_failed_insufficient_evidence.json | 14 +- ...hook__verification_failed_not_a_claim.json | 16 +- ...ification_failed_upstream_unavailable.json | 14 +- ...k__verification_needs_input_hint_none.json | 24 + ..._verification_needs_input_multi_claim.json | 14 +- .../parity/expected/account__api_root.json | 1 + .../account__ask_history_after_reset.json | 1 + .../account__ask_history_after_send.json | 1 + .../expected/account__ask_history_empty.json | 1 + .../account__ask_history_messages.json | 1 + .../parity/expected/account__ask_reset.json | 1 + .../parity/expected/account__ask_send.json | 1 + .../expected/account__ask_send_language.json | 1 + .../expected/account__library_empty.json | 9 +- ...dified_at_crosses_midnight_by_minutes.json | 25 +- ...item_modified_at_same_day_hours_apart.json | 25 +- .../expected/account__library_items.json | 63 +- .../expected/account__library_no_match.json | 9 +- .../account__library_page_past_end.json | 9 +- ...ccount__library_paginated_second_page.json | 44 +- .../account__me_usage_extra_credits.json | 65 +- .../account__me_usage_extra_only.json | 65 +- .../account__me_usage_extract_calls.json | 65 +- .../expected/account__me_usage_free.json | 65 +- .../account__me_usage_free_partly_spent.json | 65 +- .../expected/account__me_usage_oauth.json | 121 + .../expected/account__me_usage_plus.json | 65 +- .../expected/account__me_usage_pro.json | 65 +- .../expected/account__me_usage_pro_extra.json | 65 +- .../account__me_usage_stale_quota_row.json | 65 +- .../account__oauth_register_success.json | 1 + .../expected/account__oauth_revoke.json | 1 + .../expected/account__oauth_revoke_token.json | 1 + .../account__webhook_secret_api_key.json | 21 + ...nt__webhook_secret_insufficient_scope.json | 21 + .../account__webhook_secret_oauth.json | 1 + .../expected/assess__402_no_credits.json | 2 +- .../expected/assess__deep_tier_hit.json | 23 +- .../expected/assess__idempotency_first.json | 23 +- .../expected/assess__idempotency_replay.json | 23 +- .../expected/assess__list_all_error_rows.json | 37 +- .../expected/assess__list_compound_item.json | 40 +- .../expected/assess__list_mixed_rows.json | 65 +- .../parity/expected/assess__memo_hit.json | 23 +- .../expected/assess__replay_later_200.json | 23 +- .../expected/assess__single_no_claim.json | 10 +- .../expected/assess__single_one_claim.json | 23 +- ...ss__single_text_over_wave_more_claims.json | 292 +- .../assess__single_text_several_claims.json | 51 +- .../expected/assess__suggest_rewrite.json | 37 +- .../expected/citecheck__402_no_credits.json | 2 +- ...citecheck__422_webhook_secret_missing.json | 30 + .../expected/citecheck__get_checking.json | 100 +- .../citecheck__get_completed_clean.json | 110 +- .../citecheck__get_completed_doi_pair.json | 75 +- .../citecheck__get_completed_incomplete.json | 126 +- ...citecheck__get_completed_issues_found.json | 140 +- ...check__get_completed_partly_supported.json | 110 +- .../citecheck__get_completed_unchecked.json | 110 +- .../citecheck__get_failed_no_citations.json | 41 +- ...heck__get_failed_upstream_unavailable.json | 147 +- .../expected/citecheck__get_queued.json | 69 +- ...heck__get_text_limit_exactly_at_limit.json | 118 +- .../citecheck__get_text_limit_reached.json | 92 +- .../citecheck__idempotent_replay_202.json | 7 +- .../expected/citecheck__receipt_202.json | 7 +- ...echeck__receipt_202_empty_webhook_url.json | 7 +- .../errors__auth_bad_oauth_token.json | 21 + .../errors__auth_expired_oauth_token.json | 21 + .../errors__auth_oauth_issuer_off.json | 21 + .../expected/errors__auth_oauth_only.json | 21 + ...errors__method_not_allowed_get_verify.json | 21 + ...rors__method_not_allowed_post_library.json | 21 + .../errors__oauth_register_disabled.json | 21 + ...auth_register_invalid_client_metadata.json | 21 + ...__oauth_register_invalid_redirect_uri.json | 21 + .../errors__oauth_revoke_invalid_client.json | 21 + .../errors__oauth_token_invalid_client.json | 21 + .../errors__oauth_token_invalid_grant.json | 21 + ...rrors__oauth_token_missing_grant_type.json | 21 + ...rs__oauth_token_refresh_invalid_grant.json | 21 + ...s__oauth_token_unsupported_grant_type.json | 21 + .../errors__payment_required_ask.json | 2 +- .../errors__payment_required_assess.json | 2 +- .../errors__payment_required_verify.json | 2 +- ...__payment_required_verify_batch_short.json | 2 +- .../errors__web_assess_payment_required.json | 28 + .../errors__web_assess_signup_required.json | 21 + .../errors__web_review_rate_limited.json | 25 + .../parity/expected/extract__focus_ready.json | 23 +- .../expected/extract__idempotency_replay.json | 26 +- .../expected/extract__locate_all_dropped.json | 23 +- .../expected/extract__locate_failed.json | 26 +- .../extract__locate_leading_whitespace.json | 34 +- .../extract__locate_some_dropped.json | 47 +- .../parity/expected/extract__locate_true.json | 47 +- .../extract__no_match_with_focus.json | 14 +- .../parity/expected/extract__not_a_claim.json | 14 +- .../extract__not_a_claim_beside_claims.json | 23 +- .../expected/extract__ready_one_claim.json | 23 +- .../extract__ready_several_claims.json | 26 +- .../expected/extract__replay_later_200.json | 26 +- .../expected/extract__url_input_located.json | 34 +- .../expected/review__402_no_credits.json | 2 +- .../review__402_no_credits_exhausted.json | 2 +- .../review__422_webhook_secret_missing.json | 30 + .../expected/review__delete_not_a_route.json | 21 + .../expected/review__get_assessing.json | 109 +- ...view__get_assessment_rows_full_fields.json | 169 +- .../expected/review__get_citations_clean.json | 182 +- ...w__get_citations_failed_and_unchecked.json | 198 +- .../review__get_citations_issue_full.json | 204 +- ...view__get_citations_issue_issues_view.json | 92 +- ..._get_citations_limit_exactly_at_limit.json | 182 +- .../review__get_citations_limit_reached.json | 156 +- ..._get_citations_no_claim_but_citations.json | 151 +- ...w__get_citations_no_claim_no_citation.json | 67 +- .../review__get_citations_not_asked.json | 99 +- .../expected/review__get_citations_only.json | 158 +- ...eview__get_citations_partly_supported.json | 182 +- .../review__get_citations_running.json | 164 +- ...w__get_citations_skipped_switched_off.json | 99 +- ...view__get_citations_skipped_url_input.json | 67 +- .../review__get_claim_limit_not_reached.json | 129 +- ...review__get_claim_limit_reached_exact.json | 129 +- .../review__get_completed_assess_only.json | 155 +- .../expected/review__get_completed_clean.json | 129 +- ...ew__get_completed_issues_all_verified.json | 296 +- .../review__get_completed_issues_full.json | 333 +- ...iew__get_completed_issues_view_issues.json | 140 +- .../review__get_completed_positions.json | 141 +- ...review__get_completed_recovered_claim.json | 99 +- ...ew__get_completed_with_untraced_claim.json | 141 +- .../review__get_during_quick_check.json | 85 +- ...w__get_failed_every_assessment_failed.json | 156 +- .../expected/review__get_failed_no_claim.json | 67 +- .../review__get_failed_no_claim_untraced.json | 67 +- .../expected/review__get_more_claims.json | 154 +- ...dified_at_crosses_midnight_by_minutes.json | 168 +- ...tion_modified_at_same_day_hours_apart.json | 168 +- .../expected/review__get_policy_resolved.json | 97 +- .../parity/expected/review__get_queued.json | 59 +- .../expected/review__get_suggested_edits.json | 286 +- ...view__get_suggested_edits_issues_view.json | 133 +- .../expected/review__get_url_review.json | 105 +- .../expected/review__get_verifying.json | 176 +- .../review__idempotent_replay_202.json | 7 +- .../parity/expected/review__receipt_202.json | 7 +- ...review__receipt_202_empty_webhook_url.json | 7 +- .../review__receipt_202_webhook_url.json | 7 +- .../expected/review__replay_later_202.json | 7 +- .../parity/expected/verify__batch_202.json | 18 +- .../verify__batch_no_credits_402.json | 2 +- .../expected/verify__batch_partial_202.json | 15 +- .../parity/expected/verify__delete_200.json | 12 + .../parity/expected/verify__delete_404.json | 21 + .../verify__delete_not_yours_404.json | 21 + .../verify__idempotency_key_replay.json | 10 +- .../expected/verify__implicit_dedup_200.json | 7 +- .../verify__implicit_repeat_replay.json | 10 +- .../parity/expected/verify__list_200.json | 44 +- ...dified_at_crosses_midnight_by_minutes.json | 25 +- ..._200_modified_at_same_day_hours_apart.json | 25 +- .../expected/verify__list_clamped_200.json | 9 +- .../expected/verify__list_empty_200.json | 9 +- .../expected/verify__list_page2_200.json | 27 +- .../expected/verify__no_credits_402.json | 2 +- .../expected/verify__replay_later_202.json | 10 +- .../parity/expected/verify__select_202.json | 18 +- ...erify__status_cancelled_after_a_while.json | 10 + .../expected/verify__status_completed.json | 106 +- ...erify__status_completed_after_a_while.json | 106 +- ...dified_at_crosses_midnight_by_minutes.json | 102 +- ...hile_modified_at_same_day_hours_apart.json | 102 +- ...dified_at_crosses_midnight_by_minutes.json | 102 +- ...eted_modified_at_same_day_hours_apart.json | 102 +- .../expected/verify__status_failed.json | 10 + .../verify__status_failed_after_a_while.json | 10 + ...__status_failed_after_a_while_framing.json | 10 + .../verify__status_failed_retryable.json | 10 + .../expected/verify__status_needs_input.json | 17 + .../expected/verify__status_not_a_claim.json | 11 + ...ify__status_not_a_claim_after_a_while.json | 10 + .../verify__status_older_run_completed.json | 106 +- ...rify__status_older_run_failed_crashed.json | 10 + ...lder_run_failed_insufficient_evidence.json | 10 + .../verify__status_older_run_in_progress.json | 16 +- .../expected/verify__status_processing.json | 16 +- ...rify__status_processing_after_a_while.json | 16 +- .../expected/verify__status_task_stuck.json | 10 + .../parity/expected/verify__submit_202.json | 10 +- .../expected/verify__submit_202_options.json | 10 +- .../verify__submit_202_text_alias.json | 10 +- .../expected/verify__verification_200.json | 92 +- .../verify__verification_200_covered.json | 94 +- ...dified_at_crosses_midnight_by_minutes.json | 90 +- ...fy__verification_200_modified_at_null.json | 92 +- ..._200_modified_at_same_day_hours_apart.json | 90 +- ...ify__verification_200_modified_at_set.json | 90 +- ...ation_200_result_json_conclusion_only.json | 53 +- ...y__verification_200_result_json_empty.json | 53 +- ...ion_200_result_json_old_shape_strings.json | 69 +- .../verify__verification_by_task_id_200.json | 92 +- ...verify__verification_public_anonymous.json | 80 +- ...__verification_public_of_another_user.json | 92 +- ...y__verification_zero_retention_served.json | 92 +- .../verify__webhook_secret_missing_422.json | 22 + .../webhook__certificate_timestamped.json | 8 +- ..._certificate_timestamped_review_child.json | 8 +- .../webhook__citecheck_completed.json | 12 +- .../expected/webhook__citecheck_failed.json | 12 +- .../expected/webhook__review_completed.json | 28 +- ...ook__review_completed_key_default_url.json | 12 +- ...k__review_completed_oversized_rebuilt.json | 4 +- .../expected/webhook__review_failed.json | 12 +- .../parity/expected/webhook__test_fire.json | 12 + .../webhook__verification_completed.json | 6 +- ...hook__verification_completed_in_batch.json | 8 +- ...dified_at_crosses_midnight_by_minutes.json | 4 +- ...eted_modified_at_same_day_hours_apart.json | 4 +- ..._verification_completed_sparse_result.json | 48 + ...bhook__verification_failed_error_none.json | 15 + ...fication_failed_insufficient_evidence.json | 2 +- ...hook__verification_failed_not_a_claim.json | 2 +- ...ification_failed_upstream_unavailable.json | 2 +- ...k__verification_needs_input_hint_none.json | 23 + ..._verification_needs_input_multi_claim.json | 2 +- .../parity/legacy/account__api_root.json | 43 + .../account__ask_history_after_reset.json | 14 + .../account__ask_history_after_send.json | 25 + .../legacy/account__ask_history_empty.json | 14 + .../legacy/account__ask_history_messages.json | 25 + .../parity/legacy/account__ask_reset.json | 11 + .../parity/legacy/account__ask_send.json | 13 + .../legacy/account__ask_send_language.json | 13 + .../parity/legacy/account__library_empty.json | 10 +- ...dified_at_crosses_midnight_by_minutes.json | 10 +- ...item_modified_at_same_day_hours_apart.json | 10 +- .../parity/legacy/account__library_items.json | 20 +- .../legacy/account__library_no_match.json | 10 +- .../account__library_page_past_end.json | 10 +- ...ccount__library_paginated_second_page.json | 16 +- .../account__me_usage_extra_credits.json | 14 +- .../legacy/account__me_usage_extra_only.json | 14 +- .../account__me_usage_extract_calls.json | 14 +- .../parity/legacy/account__me_usage_free.json | 14 +- .../account__me_usage_free_partly_spent.json | 14 +- .../legacy/account__me_usage_oauth.json | 66 + .../parity/legacy/account__me_usage_plus.json | 14 +- .../parity/legacy/account__me_usage_pro.json | 14 +- .../legacy/account__me_usage_pro_extra.json | 14 +- .../account__me_usage_stale_quota_row.json | 14 +- .../account__oauth_register_success.json | 25 + .../parity/legacy/account__oauth_revoke.json | 9 + .../legacy/account__oauth_revoke_token.json | 9 + .../account__webhook_secret_api_key.json | 12 + ...nt__webhook_secret_insufficient_scope.json | 14 + .../legacy/account__webhook_secret_oauth.json | 11 + .../parity/legacy/assess__401_no_key.json | 6 +- .../parity/legacy/assess__402_no_credits.json | 12 +- .../parity/legacy/assess__422_blank_item.json | 8 +- .../parity/legacy/assess__422_blank_text.json | 6 +- .../legacy/assess__422_input_conflict.json | 8 +- .../legacy/assess__422_item_too_long.json | 8 +- .../legacy/assess__422_no_input_field.json | 10 +- .../legacy/assess__422_too_many_items.json | 8 +- .../legacy/assess__422_wrong_type_claim.json | 10 +- .../legacy/assess__422_wrong_type_claims.json | 10 +- .../legacy/assess__502_framing_failed.json | 10 +- .../parity/legacy/assess__503_capacity.json | 12 +- ...ss__503_list_all_upstream_unavailable.json | 12 +- .../assess__503_upstream_unavailable.json | 12 +- .../parity/legacy/assess__deep_tier_hit.json | 10 +- .../assess__idempotency_body_mismatch.json | 6 +- .../legacy/assess__idempotency_first.json | 10 +- .../legacy/assess__idempotency_replay.json | 10 +- .../legacy/assess__list_all_error_rows.json | 10 +- .../legacy/assess__list_compound_item.json | 10 +- .../legacy/assess__list_mixed_rows.json | 10 +- .../parity/legacy/assess__memo_hit.json | 10 +- .../legacy/assess__replay_later_200.json | 10 +- .../legacy/assess__single_no_claim.json | 10 +- .../legacy/assess__single_one_claim.json | 10 +- ...ss__single_text_over_wave_more_claims.json | 10 +- .../assess__single_text_several_claims.json | 10 +- .../legacy/assess__suggest_rewrite.json | 10 +- .../legacy/citecheck__402_no_credits.json | 12 +- .../legacy/citecheck__422_empty_body.json | 10 +- .../legacy/citecheck__422_invalid_doi.json | 10 +- .../citecheck__422_max_citations_over_20.json | 10 +- ...techeck__422_max_citations_with_pairs.json | 10 +- .../citecheck__422_pair_bad_url_scheme.json | 10 +- .../legacy/citecheck__422_pair_no_source.json | 10 +- .../citecheck__422_pair_url_and_doi.json | 10 +- .../legacy/citecheck__422_pairs_empty.json | 10 +- .../legacy/citecheck__422_pairs_over_20.json | 10 +- ...citecheck__422_quote_not_in_statement.json | 10 +- .../legacy/citecheck__422_text_and_pairs.json | 10 +- .../legacy/citecheck__422_unknown_field.json | 10 +- .../legacy/citecheck__422_url_input.json | 10 +- ...citecheck__422_webhook_secret_missing.json | 21 + .../citecheck__429_citecheck_in_flight.json | 10 +- .../legacy/citecheck__503_capacity.json | 12 +- .../citecheck__503_citations_unavailable.json | 12 +- .../legacy/citecheck__get_404_not_found.json | 8 +- .../legacy/citecheck__get_404_review_id.json | 8 +- .../legacy/citecheck__get_410_purged.json | 10 +- .../legacy/citecheck__get_checking.json | 12 +- .../citecheck__get_completed_clean.json | 14 +- .../citecheck__get_completed_doi_pair.json | 14 +- .../citecheck__get_completed_incomplete.json | 14 +- ...citecheck__get_completed_issues_found.json | 14 +- ...check__get_completed_partly_supported.json | 14 +- .../citecheck__get_completed_unchecked.json | 14 +- .../citecheck__get_failed_no_citations.json | 14 +- ...heck__get_failed_upstream_unavailable.json | 14 +- .../parity/legacy/citecheck__get_queued.json | 12 +- ...heck__get_text_limit_exactly_at_limit.json | 14 +- .../citecheck__get_text_limit_reached.json | 14 +- ...echeck__idempotency_body_mismatch_422.json | 10 +- .../citecheck__idempotency_conflict_409.json | 10 +- .../citecheck__idempotent_replay_202.json | 8 +- .../parity/legacy/citecheck__receipt_202.json | 8 +- ...echeck__receipt_202_empty_webhook_url.json | 8 +- .../legacy/errors__ask_bad_language.json | 6 +- .../legacy/errors__ask_empty_message.json | 6 +- .../parity/legacy/errors__ask_failed.json | 10 +- .../errors__ask_idempotency_key_reused.json | 6 +- .../legacy/errors__ask_message_too_long.json | 6 +- .../legacy/errors__ask_not_completed.json | 6 +- .../legacy/errors__ask_reset_not_found.json | 6 +- .../legacy/errors__ask_send_not_found.json | 6 +- .../parity/legacy/errors__auth_bad_key.json | 6 +- .../legacy/errors__auth_bad_oauth_token.json | 13 + .../legacy/errors__auth_basic_scheme.json | 6 +- .../errors__auth_expired_oauth_token.json | 13 + .../errors__auth_insufficient_scope.json | 10 +- .../legacy/errors__auth_missing_bearer.json | 6 +- .../errors__auth_missing_bearer_post.json | 6 +- .../legacy/errors__auth_oauth_issuer_off.json | 11 + .../legacy/errors__auth_oauth_only.json | 12 + ...rrors__auth_private_claim_ask_history.json | 10 +- .../errors__auth_private_claim_ask_send.json | 10 +- .../errors__auth_unrecognised_prefix.json | 6 +- ...rrors__http_error_library_bad_curated.json | 6 +- ...rrors__http_error_library_bad_verdict.json | 6 +- ...errors__method_not_allowed_get_verify.json | 11 + ...rors__method_not_allowed_post_library.json | 11 + .../parity/legacy/errors__not_found_ask.json | 6 +- .../legacy/errors__not_found_route.json | 6 +- .../errors__not_found_verification.json | 6 +- .../errors__not_found_verify_status.json | 6 +- .../errors__oauth_register_disabled.json | 11 + ...auth_register_invalid_client_metadata.json | 12 + ...__oauth_register_invalid_redirect_uri.json | 12 + .../errors__oauth_revoke_invalid_client.json | 13 + .../errors__oauth_token_invalid_client.json | 13 + .../errors__oauth_token_invalid_grant.json | 12 + ...rrors__oauth_token_missing_grant_type.json | 11 + ...rs__oauth_token_refresh_invalid_grant.json | 12 + ...s__oauth_token_unsupported_grant_type.json | 11 + .../legacy/errors__payment_required_ask.json | 12 +- .../errors__payment_required_assess.json | 12 +- .../errors__payment_required_verify.json | 12 +- ...__payment_required_verify_batch_short.json | 12 +- .../legacy/errors__rate_limited_extract.json | 12 +- .../errors__service_unavailable_ask.json | 12 +- .../errors__service_unavailable_capacity.json | 12 +- .../errors__validation_malformed_json.json | 6 +- ...errors__validation_missing_field_hint.json | 10 +- ...errors__validation_unknown_field_only.json | 10 +- ...__validation_unknown_field_wrong_type.json | 10 +- .../legacy/errors__validation_wrong_type.json | 10 +- .../errors__web_assess_payment_required.json | 19 + .../errors__web_assess_signup_required.json | 14 + .../errors__web_review_rate_limited.json | 17 + .../parity/legacy/extract__401_no_key.json | 6 +- .../legacy/extract__422_blank_text.json | 6 +- .../legacy/extract__422_focus_too_long.json | 6 +- .../legacy/extract__422_missing_text.json | 10 +- .../legacy/extract__429_daily_limit.json | 12 +- .../extract__502_extraction_failed.json | 10 +- .../extract__503_upstream_unavailable.json | 12 +- .../parity/legacy/extract__focus_ready.json | 10 +- ...xtract__idempotency_body_mismatch_422.json | 6 +- .../legacy/extract__idempotency_replay.json | 10 +- .../legacy/extract__locate_all_dropped.json | 10 +- .../parity/legacy/extract__locate_failed.json | 10 +- .../extract__locate_leading_whitespace.json | 10 +- .../legacy/extract__locate_some_dropped.json | 10 +- .../parity/legacy/extract__locate_true.json | 10 +- .../legacy/extract__no_match_with_focus.json | 10 +- .../parity/legacy/extract__not_a_claim.json | 10 +- .../extract__not_a_claim_beside_claims.json | 10 +- .../legacy/extract__ready_one_claim.json | 10 +- .../legacy/extract__ready_several_claims.json | 10 +- .../legacy/extract__replay_later_200.json | 10 +- .../extract__url_fetch_502_unreadable.json | 10 +- .../extract__url_fetch_503_upstream.json | 12 +- .../legacy/extract__url_input_located.json | 10 +- .../legacy/review__401_no_credentials.json | 6 +- .../parity/legacy/review__402_no_credits.json | 12 +- .../review__402_no_credits_exhausted.json | 12 +- .../parity/legacy/review__422_blank_text.json | 10 +- .../review__422_citations_options_object.json | 10 +- .../legacy/review__422_depth_unknown.json | 10 +- .../review__422_invalid_confidence_band.json | 10 +- .../review__422_invalid_verdict_label.json | 10 +- .../review__422_max_assessments_negative.json | 10 +- .../review__422_max_citations_negative.json | 10 +- .../review__422_max_citations_too_big.json | 10 +- ...review__422_max_verifications_too_big.json | 10 +- .../legacy/review__422_text_missing.json | 10 +- .../legacy/review__422_text_wrong_type.json | 10 +- .../review__422_unknown_escalate_field.json | 10 +- .../review__422_unknown_top_level_field.json | 10 +- .../review__422_unsupported_language.json | 10 +- .../legacy/review__422_visibility_public.json | 10 +- .../review__422_webhook_secret_missing.json | 21 + .../review__429_extract_daily_limit.json | 12 +- .../legacy/review__429_review_in_flight.json | 10 +- .../parity/legacy/review__503_capacity.json | 12 +- .../review__503_citations_capacity.json | 12 +- .../legacy/review__delete_not_a_route.json | 11 + .../review__get_403_insufficient_scope.json | 10 +- .../legacy/review__get_404_not_found.json | 8 +- .../legacy/review__get_404_other_account.json | 8 +- .../parity/legacy/review__get_410_purged.json | 10 +- .../legacy/review__get_422_unknown_view.json | 10 +- .../parity/legacy/review__get_assessing.json | 14 +- ...view__get_assessment_rows_full_fields.json | 14 +- .../legacy/review__get_citations_clean.json | 14 +- ...w__get_citations_failed_and_unchecked.json | 14 +- .../review__get_citations_issue_full.json | 14 +- ...view__get_citations_issue_issues_view.json | 14 +- ..._get_citations_limit_exactly_at_limit.json | 14 +- .../review__get_citations_limit_reached.json | 14 +- ..._get_citations_no_claim_but_citations.json | 14 +- ...w__get_citations_no_claim_no_citation.json | 14 +- .../review__get_citations_not_asked.json | 14 +- .../legacy/review__get_citations_only.json | 14 +- ...eview__get_citations_partly_supported.json | 14 +- .../legacy/review__get_citations_running.json | 12 +- ...w__get_citations_skipped_switched_off.json | 14 +- ...view__get_citations_skipped_url_input.json | 14 +- .../review__get_claim_limit_not_reached.json | 14 +- ...review__get_claim_limit_reached_exact.json | 14 +- .../review__get_completed_assess_only.json | 14 +- .../legacy/review__get_completed_clean.json | 14 +- ...ew__get_completed_issues_all_verified.json | 46 +- .../review__get_completed_issues_full.json | 32 +- ...iew__get_completed_issues_view_issues.json | 20 +- .../review__get_completed_positions.json | 14 +- ...review__get_completed_recovered_claim.json | 14 +- ...ew__get_completed_with_untraced_claim.json | 14 +- .../review__get_during_quick_check.json | 12 +- ...w__get_failed_every_assessment_failed.json | 14 +- .../legacy/review__get_failed_no_claim.json | 14 +- .../review__get_failed_no_claim_untraced.json | 14 +- .../legacy/review__get_more_claims.json | 14 +- ...dified_at_crosses_midnight_by_minutes.json | 28 +- ...tion_modified_at_same_day_hours_apart.json | 28 +- .../legacy/review__get_policy_resolved.json | 14 +- .../parity/legacy/review__get_queued.json | 12 +- .../legacy/review__get_suggested_edits.json | 28 +- ...view__get_suggested_edits_issues_view.json | 18 +- .../parity/legacy/review__get_url_review.json | 14 +- .../parity/legacy/review__get_verifying.json | 12 +- ...review__idempotency_body_mismatch_422.json | 10 +- .../review__idempotency_conflict_409.json | 10 +- ...mpotency_conflict_409_existing_review.json | 10 +- .../legacy/review__idempotent_replay_202.json | 8 +- .../parity/legacy/review__receipt_202.json | 8 +- ...review__receipt_202_empty_webhook_url.json | 8 +- .../review__receipt_202_webhook_url.json | 8 +- .../legacy/review__replay_later_202.json | 8 +- .../parity/legacy/verify__batch_202.json | 14 +- .../legacy/verify__batch_capacity_503.json | 12 +- .../legacy/verify__batch_empty_422.json | 6 +- ...erify__batch_idempotency_conflict_409.json | 8 +- .../verify__batch_item_missing_claim_422.json | 10 +- .../legacy/verify__batch_no_credits_402.json | 12 +- .../legacy/verify__batch_partial_202.json | 12 +- .../legacy/verify__batch_too_many_422.json | 6 +- .../legacy/verify__blank_claim_422.json | 6 +- .../parity/legacy/verify__capacity_503.json | 12 +- .../parity/legacy/verify__delete_200.json | 11 + .../parity/legacy/verify__delete_404.json | 11 + .../legacy/verify__delete_not_yours_404.json | 11 + ...verify__idempotency_body_mismatch_422.json | 6 +- .../verify__idempotency_conflict_409.json | 8 +- .../verify__idempotency_key_replay.json | 10 +- .../legacy/verify__implicit_dedup_200.json | 8 +- .../verify__implicit_repeat_replay.json | 10 +- .../legacy/verify__invalid_depth_422.json | 10 +- .../legacy/verify__invalid_language_422.json | 6 +- .../parity/legacy/verify__list_200.json | 16 +- ...dified_at_crosses_midnight_by_minutes.json | 10 +- ..._200_modified_at_same_day_hours_apart.json | 10 +- .../legacy/verify__list_clamped_200.json | 10 +- .../parity/legacy/verify__list_empty_200.json | 10 +- .../parity/legacy/verify__list_page2_200.json | 12 +- .../legacy/verify__misnamed_field_422.json | 10 +- .../legacy/verify__missing_claim_422.json | 10 +- .../parity/legacy/verify__no_credits_402.json | 12 +- .../legacy/verify__replay_later_202.json | 10 +- .../parity/legacy/verify__select_202.json | 14 +- .../legacy/verify__select_empty_422.json | 6 +- .../verify__select_invalid_selection_422.json | 10 +- ...rify__select_no_selection_pending_409.json | 10 +- .../legacy/verify__select_too_many_422.json | 6 +- .../verify__select_unknown_task_404.json | 6 +- ...erify__status_cancelled_after_a_while.json | 10 +- .../legacy/verify__status_completed.json | 14 +- ...erify__status_completed_after_a_while.json | 14 +- ...dified_at_crosses_midnight_by_minutes.json | 12 +- ...hile_modified_at_same_day_hours_apart.json | 12 +- ...dified_at_crosses_midnight_by_minutes.json | 12 +- ...eted_modified_at_same_day_hours_apart.json | 12 +- .../parity/legacy/verify__status_failed.json | 10 +- .../verify__status_failed_after_a_while.json | 10 +- ...__status_failed_after_a_while_framing.json | 10 +- .../verify__status_failed_retryable.json | 10 +- .../legacy/verify__status_needs_input.json | 10 +- .../legacy/verify__status_not_a_claim.json | 10 +- ...ify__status_not_a_claim_after_a_while.json | 10 +- .../legacy/verify__status_not_yours_404.json | 6 +- .../verify__status_older_run_completed.json | 14 +- ...rify__status_older_run_failed_crashed.json | 10 +- ...lder_run_failed_insufficient_evidence.json | 10 +- .../verify__status_older_run_in_progress.json | 12 +- .../legacy/verify__status_processing.json | 12 +- ...rify__status_processing_after_a_while.json | 12 +- .../legacy/verify__status_task_stuck.json | 10 +- .../legacy/verify__status_unknown_404.json | 6 +- .../parity/legacy/verify__submit_202.json | 10 +- .../legacy/verify__submit_202_options.json | 10 +- .../legacy/verify__submit_202_text_alias.json | 10 +- .../legacy/verify__unauthenticated_401.json | 6 +- .../legacy/verify__verification_200.json | 12 +- .../verify__verification_200_covered.json | 16 +- ...dified_at_crosses_midnight_by_minutes.json | 10 +- ...fy__verification_200_modified_at_null.json | 12 +- ..._200_modified_at_same_day_hours_apart.json | 10 +- ...ify__verification_200_modified_at_set.json | 10 +- ...ation_200_result_json_conclusion_only.json | 12 +- ...y__verification_200_result_json_empty.json | 12 +- ...ion_200_result_json_old_shape_strings.json | 12 +- .../legacy/verify__verification_404.json | 6 +- .../verify__verification_by_task_id_200.json | 12 +- .../verify__verification_failed_409.json | 10 +- ..._failed_409_not_a_claim_after_a_while.json | 10 +- .../verify__verification_not_ready_409.json | 10 +- .../verify__verification_not_yours_404.json | 6 +- ...verify__verification_public_anonymous.json | 12 +- ...__verification_public_of_another_user.json | 12 +- ...verify__verification_unknown_task_404.json | 8 +- ...rification_zero_retention_expired_410.json | 10 +- ...y__verification_zero_retention_served.json | 12 +- .../verify__webhook_secret_missing_422.json | 12 + .../webhook__certificate_timestamped.json | 12 +- ..._certificate_timestamped_review_child.json | 12 +- .../legacy/webhook__citecheck_completed.json | 16 +- .../legacy/webhook__citecheck_failed.json | 16 +- .../legacy/webhook__review_completed.json | 32 +- ...ook__review_completed_key_default_url.json | 16 +- ...k__review_completed_oversized_rebuilt.json | 8 +- .../parity/legacy/webhook__review_failed.json | 16 +- .../parity/legacy/webhook__test_fire.json | 17 + .../webhook__verification_completed.json | 10 +- ...hook__verification_completed_in_batch.json | 12 +- ...dified_at_crosses_midnight_by_minutes.json | 8 +- ...eted_modified_at_same_day_hours_apart.json | 8 +- ..._verification_completed_sparse_result.json | 52 + ...bhook__verification_failed_error_none.json | 19 + ...fication_failed_insufficient_evidence.json | 6 +- ...hook__verification_failed_not_a_claim.json | 6 +- ...ification_failed_upstream_unavailable.json | 6 +- ...k__verification_needs_input_hint_none.json | 27 + ..._verification_needs_input_multi_claim.json | 6 +- tests/fixtures/parity/pickles.json | 156 + tests/fixtures/parity/schemas.json | 26564 ++++++++++++++++ tests/parity_generate.py | 8 + tests/parity_observe.py | 28 +- tests/parity_static.py | 46 + tests/test_parity.py | 79 +- tests/test_read_both_shapes.py | 77 +- 944 files changed, 44136 insertions(+), 4731 deletions(-) create mode 100644 tests/fixtures/parity/canonical/account__api_root.json create mode 100644 tests/fixtures/parity/canonical/account__ask_history_after_reset.json create mode 100644 tests/fixtures/parity/canonical/account__ask_history_after_send.json create mode 100644 tests/fixtures/parity/canonical/account__ask_history_empty.json create mode 100644 tests/fixtures/parity/canonical/account__ask_history_messages.json create mode 100644 tests/fixtures/parity/canonical/account__ask_reset.json create mode 100644 tests/fixtures/parity/canonical/account__ask_send.json create mode 100644 tests/fixtures/parity/canonical/account__ask_send_language.json create mode 100644 tests/fixtures/parity/canonical/account__me_usage_oauth.json create mode 100644 tests/fixtures/parity/canonical/account__oauth_register_success.json create mode 100644 tests/fixtures/parity/canonical/account__oauth_revoke.json create mode 100644 tests/fixtures/parity/canonical/account__oauth_revoke_token.json create mode 100644 tests/fixtures/parity/canonical/account__webhook_secret_api_key.json create mode 100644 tests/fixtures/parity/canonical/account__webhook_secret_insufficient_scope.json create mode 100644 tests/fixtures/parity/canonical/account__webhook_secret_oauth.json create mode 100644 tests/fixtures/parity/canonical/citecheck__422_webhook_secret_missing.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_bad_oauth_token.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_expired_oauth_token.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_oauth_issuer_off.json create mode 100644 tests/fixtures/parity/canonical/errors__auth_oauth_only.json create mode 100644 tests/fixtures/parity/canonical/errors__method_not_allowed_get_verify.json create mode 100644 tests/fixtures/parity/canonical/errors__method_not_allowed_post_library.json create mode 100644 tests/fixtures/parity/canonical/errors__oauth_register_disabled.json create mode 100644 tests/fixtures/parity/canonical/errors__oauth_register_invalid_client_metadata.json create mode 100644 tests/fixtures/parity/canonical/errors__oauth_register_invalid_redirect_uri.json create mode 100644 tests/fixtures/parity/canonical/errors__oauth_revoke_invalid_client.json create mode 100644 tests/fixtures/parity/canonical/errors__oauth_token_invalid_client.json create mode 100644 tests/fixtures/parity/canonical/errors__oauth_token_invalid_grant.json create mode 100644 tests/fixtures/parity/canonical/errors__oauth_token_missing_grant_type.json create mode 100644 tests/fixtures/parity/canonical/errors__oauth_token_refresh_invalid_grant.json create mode 100644 tests/fixtures/parity/canonical/errors__oauth_token_unsupported_grant_type.json create mode 100644 tests/fixtures/parity/canonical/errors__web_assess_payment_required.json create mode 100644 tests/fixtures/parity/canonical/errors__web_assess_signup_required.json create mode 100644 tests/fixtures/parity/canonical/errors__web_review_rate_limited.json create mode 100644 tests/fixtures/parity/canonical/review__422_webhook_secret_missing.json create mode 100644 tests/fixtures/parity/canonical/review__delete_not_a_route.json create mode 100644 tests/fixtures/parity/canonical/verify__delete_200.json create mode 100644 tests/fixtures/parity/canonical/verify__delete_404.json create mode 100644 tests/fixtures/parity/canonical/verify__delete_not_yours_404.json create mode 100644 tests/fixtures/parity/canonical/verify__webhook_secret_missing_422.json create mode 100644 tests/fixtures/parity/canonical/webhook__test_fire.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_completed_sparse_result.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_failed_error_none.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_needs_input_hint_none.json create mode 100644 tests/fixtures/parity/expected/account__api_root.json create mode 100644 tests/fixtures/parity/expected/account__ask_history_after_reset.json create mode 100644 tests/fixtures/parity/expected/account__ask_history_after_send.json create mode 100644 tests/fixtures/parity/expected/account__ask_history_empty.json create mode 100644 tests/fixtures/parity/expected/account__ask_history_messages.json create mode 100644 tests/fixtures/parity/expected/account__ask_reset.json create mode 100644 tests/fixtures/parity/expected/account__ask_send.json create mode 100644 tests/fixtures/parity/expected/account__ask_send_language.json create mode 100644 tests/fixtures/parity/expected/account__me_usage_oauth.json create mode 100644 tests/fixtures/parity/expected/account__oauth_register_success.json create mode 100644 tests/fixtures/parity/expected/account__oauth_revoke.json create mode 100644 tests/fixtures/parity/expected/account__oauth_revoke_token.json create mode 100644 tests/fixtures/parity/expected/account__webhook_secret_api_key.json create mode 100644 tests/fixtures/parity/expected/account__webhook_secret_insufficient_scope.json create mode 100644 tests/fixtures/parity/expected/account__webhook_secret_oauth.json create mode 100644 tests/fixtures/parity/expected/citecheck__422_webhook_secret_missing.json create mode 100644 tests/fixtures/parity/expected/errors__auth_bad_oauth_token.json create mode 100644 tests/fixtures/parity/expected/errors__auth_expired_oauth_token.json create mode 100644 tests/fixtures/parity/expected/errors__auth_oauth_issuer_off.json create mode 100644 tests/fixtures/parity/expected/errors__auth_oauth_only.json create mode 100644 tests/fixtures/parity/expected/errors__method_not_allowed_get_verify.json create mode 100644 tests/fixtures/parity/expected/errors__method_not_allowed_post_library.json create mode 100644 tests/fixtures/parity/expected/errors__oauth_register_disabled.json create mode 100644 tests/fixtures/parity/expected/errors__oauth_register_invalid_client_metadata.json create mode 100644 tests/fixtures/parity/expected/errors__oauth_register_invalid_redirect_uri.json create mode 100644 tests/fixtures/parity/expected/errors__oauth_revoke_invalid_client.json create mode 100644 tests/fixtures/parity/expected/errors__oauth_token_invalid_client.json create mode 100644 tests/fixtures/parity/expected/errors__oauth_token_invalid_grant.json create mode 100644 tests/fixtures/parity/expected/errors__oauth_token_missing_grant_type.json create mode 100644 tests/fixtures/parity/expected/errors__oauth_token_refresh_invalid_grant.json create mode 100644 tests/fixtures/parity/expected/errors__oauth_token_unsupported_grant_type.json create mode 100644 tests/fixtures/parity/expected/errors__web_assess_payment_required.json create mode 100644 tests/fixtures/parity/expected/errors__web_assess_signup_required.json create mode 100644 tests/fixtures/parity/expected/errors__web_review_rate_limited.json create mode 100644 tests/fixtures/parity/expected/review__422_webhook_secret_missing.json create mode 100644 tests/fixtures/parity/expected/review__delete_not_a_route.json create mode 100644 tests/fixtures/parity/expected/verify__delete_200.json create mode 100644 tests/fixtures/parity/expected/verify__delete_404.json create mode 100644 tests/fixtures/parity/expected/verify__delete_not_yours_404.json create mode 100644 tests/fixtures/parity/expected/verify__webhook_secret_missing_422.json create mode 100644 tests/fixtures/parity/expected/webhook__test_fire.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_completed_sparse_result.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_failed_error_none.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_needs_input_hint_none.json create mode 100644 tests/fixtures/parity/legacy/account__api_root.json create mode 100644 tests/fixtures/parity/legacy/account__ask_history_after_reset.json create mode 100644 tests/fixtures/parity/legacy/account__ask_history_after_send.json create mode 100644 tests/fixtures/parity/legacy/account__ask_history_empty.json create mode 100644 tests/fixtures/parity/legacy/account__ask_history_messages.json create mode 100644 tests/fixtures/parity/legacy/account__ask_reset.json create mode 100644 tests/fixtures/parity/legacy/account__ask_send.json create mode 100644 tests/fixtures/parity/legacy/account__ask_send_language.json create mode 100644 tests/fixtures/parity/legacy/account__me_usage_oauth.json create mode 100644 tests/fixtures/parity/legacy/account__oauth_register_success.json create mode 100644 tests/fixtures/parity/legacy/account__oauth_revoke.json create mode 100644 tests/fixtures/parity/legacy/account__oauth_revoke_token.json create mode 100644 tests/fixtures/parity/legacy/account__webhook_secret_api_key.json create mode 100644 tests/fixtures/parity/legacy/account__webhook_secret_insufficient_scope.json create mode 100644 tests/fixtures/parity/legacy/account__webhook_secret_oauth.json create mode 100644 tests/fixtures/parity/legacy/citecheck__422_webhook_secret_missing.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_bad_oauth_token.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_expired_oauth_token.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_oauth_issuer_off.json create mode 100644 tests/fixtures/parity/legacy/errors__auth_oauth_only.json create mode 100644 tests/fixtures/parity/legacy/errors__method_not_allowed_get_verify.json create mode 100644 tests/fixtures/parity/legacy/errors__method_not_allowed_post_library.json create mode 100644 tests/fixtures/parity/legacy/errors__oauth_register_disabled.json create mode 100644 tests/fixtures/parity/legacy/errors__oauth_register_invalid_client_metadata.json create mode 100644 tests/fixtures/parity/legacy/errors__oauth_register_invalid_redirect_uri.json create mode 100644 tests/fixtures/parity/legacy/errors__oauth_revoke_invalid_client.json create mode 100644 tests/fixtures/parity/legacy/errors__oauth_token_invalid_client.json create mode 100644 tests/fixtures/parity/legacy/errors__oauth_token_invalid_grant.json create mode 100644 tests/fixtures/parity/legacy/errors__oauth_token_missing_grant_type.json create mode 100644 tests/fixtures/parity/legacy/errors__oauth_token_refresh_invalid_grant.json create mode 100644 tests/fixtures/parity/legacy/errors__oauth_token_unsupported_grant_type.json create mode 100644 tests/fixtures/parity/legacy/errors__web_assess_payment_required.json create mode 100644 tests/fixtures/parity/legacy/errors__web_assess_signup_required.json create mode 100644 tests/fixtures/parity/legacy/errors__web_review_rate_limited.json create mode 100644 tests/fixtures/parity/legacy/review__422_webhook_secret_missing.json create mode 100644 tests/fixtures/parity/legacy/review__delete_not_a_route.json create mode 100644 tests/fixtures/parity/legacy/verify__delete_200.json create mode 100644 tests/fixtures/parity/legacy/verify__delete_404.json create mode 100644 tests/fixtures/parity/legacy/verify__delete_not_yours_404.json create mode 100644 tests/fixtures/parity/legacy/verify__webhook_secret_missing_422.json create mode 100644 tests/fixtures/parity/legacy/webhook__test_fire.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_completed_sparse_result.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_failed_error_none.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_needs_input_hint_none.json create mode 100644 tests/fixtures/parity/pickles.json create mode 100644 tests/fixtures/parity/schemas.json create mode 100644 tests/parity_static.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7770c25..02166a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,11 @@ on upgrade. - **Reads both response shapes.** The API is adding a newer, dated response shape that gives each field one name across every endpoint. This release still asks for the original shape (it sends the same `X-Lenz-API-Version` - as before), and every model now reads either shape. The newer names are - attributes now, filled from whichever shape arrives: + as before), and every model now reads either shape. A body is read as the + newer shape only when it carries something only that shape has; every other + body is parsed exactly as before (same fields, values, dump, `repr`, + `exclude_unset`, schema and pickle). The newer names are read-only + properties, computed from whichever shape arrived, not model fields: - `ExtractedClaims.claims`: every claim found, always a list, each an `ExtractedClaim` with `claim` and (with `locate=True`) `positions`. - `AssessClaim.status` (`"completed"` / `"failed"`), `AssessClaim.failure` @@ -29,10 +32,13 @@ on upgrade. `ReviewVerification`. - `FailureBlock.code` and `FailureBlock.detail`; `ReviewAssessment.more_claims`; `ReviewSummary.claims_found`, - `claim_limit_exceeded` and `citation_limit_exceeded`; + `claim_limit_exceeded` (``None`` from the original shape, which says only + that the limit was reached: read `more_claims` there) and + `citation_limit_exceeded`; `CitecheckSummary.citation_limit_exceeded`. - - Webhooks: `event_id` on every event, `VerificationFailed.failure`, - `VerificationNeedsInput.reason` and `.claims`. A review or citation-check + - Webhooks: an `event_id` property on every event, + `VerificationFailed.failure`, `VerificationNeedsInput.reason` and + `.claims` (properties: `dataclasses.asdict` and `repr` are unchanged). A review or citation-check event in the newer shape carries no `task_id` (it reads `""`; deduplicate on `event_id`, as before). `parse_webhook` and `LenzWebhooks.parse` read the newer envelope too (`event`, `event_id`, @@ -54,8 +60,8 @@ on upgrade. `FailureBlock.failure_reason`; `ReviewAssessment.error_code` / `hint` / `identified_claims`; `claim_limit_reached` / `citation_limit_reached`; `Usage.quota_resets_at` and the `verify` / `ask` / `assess` blocks (computed - from `credits` and `costs` when a response leaves them out). They are marked - deprecated in the JSON schema; reading them does not warn. "Nothing + from `credits` and `costs` when a response leaves them out). Reading them + does not warn, and the JSON schema is unchanged. "Nothing checkable" keeps its old spelling in the old fields (`not_a_claim`, `no_claim`) and reads `no_checkable_claim` in the new ones; `ExtractedClaims.status` keeps `not_a_claim`. @@ -69,9 +75,20 @@ on upgrade. changes now; leaving it out keeps that meaning on later API versions, where `""` means "no webhook". `review` and `citecheck` send `webhook_url` exactly as before (there `""` means "no webhook"). -- `model_dump()` returns the shape the server sent: attributes filled in from - the other shape are left out, so an original-shape response dumps exactly as - before. +- A newer-shape body's `model_dump()` holds what the server sent plus the + original fields filled in from it. + +### Before the SDK asks for the newer shape + +This release only reads the newer shape; it keeps asking for the original +one. Two things to settle before a release sends the newer date: + +- A 422's `code` there is the server's new one (`blank_input` where the + original said `blank_item`, `validation_error` where it said nothing), and + its `message` is the server's sentence, not the field list. +- `lenz verify --json` on a `needs_input` run prints each option as the model + dumps it: from the newer shape that is `{claim, domain, text}`, not + `{text, domain}`. ### Correction @@ -81,6 +98,9 @@ on upgrade. They are deprecated and kept for existing callers; the API keeps sending them to integrations built against its original shape. Their deprecation warnings no longer name a date. +- **`TaskStatus.candidates` and `similar_claims` are not removed on + 2026-11-29** either (2.18.0 said so). They are deprecated, always empty, and + kept. ## [2.20.0] - 2026-10-05 diff --git a/README.md b/README.md index 502a192..02cc3ae 100644 --- a/README.md +++ b/README.md @@ -492,8 +492,9 @@ older ones keep working, with the meaning they always had: | `Usage.credits` and `Usage.costs` | the `verify` / `ask` / `assess` blocks, `quota_resets_at` | "Nothing checkable" is `no_checkable_claim` in the newer names; the older -fields keep their own spelling (`not_a_claim`, `no_claim`). `model_dump()` -returns the shape the server sent. +fields keep their own spelling (`not_a_claim`, `no_claim`). The newer names +are read-only properties, so an original-shape response parses, dumps and +compares exactly as before. ### A suggested rewrite (`suggested_rewrite`) diff --git a/openapi.json b/openapi.json index 925314c..603db53 100644 --- a/openapi.json +++ b/openapi.json @@ -411,7 +411,7 @@ } } }, - "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\n``credits.bonus`` is the deprecated old name of ``credits.extra``, the same\nnumber, kept for existing callers with the fields below. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15 (#664), while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated.** ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nThey are kept for existing callers, with the per-block ``credits`` alias.\nNew code derives them instead::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias, on the same date.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.", + "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\n``credits.bonus`` is the deprecated old name of ``credits.extra``, the same\nnumber, kept for existing callers with the fields below. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15 (#664), while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated.** ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nThey are kept for existing callers, with the per-block ``credits`` alias.\nNew code derives them instead::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.", "tags": [ "Account" ], diff --git a/src/lenz_io/cli/render.py b/src/lenz_io/cli/render.py index 753cfca..80e7e05 100644 --- a/src/lenz_io/cli/render.py +++ b/src/lenz_io/cli/render.py @@ -109,12 +109,15 @@ def render_extract(out: Output, result: ExtractedClaims) -> None: if out.json_mode: out.emit_json(_model_json(result)) return - # ``claims`` is the whole list, from either response shape (the model - # builds it from ``claim`` + ``identified_claims`` when that is what the - # server sent). Blank and repeated entries are dropped. - claims: list[str] = [] - for item in result.claims: - c = (item.claim or "").strip() + # The server splits a claim set across two fields: the primary claim lands + # in ``claim`` and any extras in ``identified_claims``. Neither alone is the + # full list — the primary is usually NOT echoed into ``identified_claims`` — + # so render the union so the primary is never dropped (and the count is + # right). Single-claim input → just ``claim``. + primary = (getattr(result, "claim", "") or "").strip() + claims = [primary] if primary else [] + for c in result.identified_claims or []: + c = (c or "").strip() if c and c not in claims: claims.append(c) if len(claims) > 1: @@ -148,7 +151,7 @@ def _render_positions(out: Output, result: ExtractedClaims, claim: str, *, inden input, where there is nothing to index) and the passage itself. The passage is the user's own text, so it prints with ``markup=False``. """ - for loc in result.claims: + for loc in result.locations or []: if (loc.claim or "").strip() != claim: continue for pos in loc.positions or []: @@ -168,10 +171,9 @@ def render_assess(out: Output, result: AssessResponse) -> None: return for c in claims: color = _VERDICT_COLOR.get(c.verdict, "white") - # A failed row (list form) names its cause in place of the confidence, - # which is meaningless there. ``error_code`` is the cause in its - # original spelling, from either response shape. - detail = c.error_code if c.status == "failed" and c.error_code else c.confidence + # An Error row (list form) names its cause in place of the confidence, + # which is meaningless there. + detail = c.error_code if c.verdict == "Error" and c.error_code else c.confidence out.console.print(f"[{color}]{c.verdict or '?'}[/{color}] ({detail}) — {c.claim}") # The reviewers' notes, as plain text: `markup=False` because the # words are a model's, and a stray "[bold]" in them is not ours. @@ -179,9 +181,9 @@ def render_assess(out: Output, result: AssessResponse) -> None: out.console.print(f" {c.rationale}", markup=False, highlight=False) if c.dissent: out.console.print(f" One reviewer disagreed: {c.dissent}", markup=False, highlight=False) - if c.more_claims: + if c.identified_claims: out.console.print(" [dim]also found:[/dim]") - for other in c.more_claims: + for other in c.identified_claims: out.console.print(f" • {other}") if c.hint: out.console.print(f" [dim]{c.hint}[/dim]") @@ -393,7 +395,7 @@ def render_task_status(out: Output, st: TaskStatus, *, task_id: str = "") -> Non if st.claims: out.console.print("[dim]claims found:[/dim]") for i, claim in enumerate(st.claims, 1): - out.console.print(f" {i}. {claim.claim}") + out.console.print(f" {i}. {claim.text}") ref = task_id or "" # Non-interactive resolution (agents/scripts): `--claim` picks by index # and `--detach` returns the spawned task_id(s) without blocking. Drop diff --git a/src/lenz_io/cli/verify.py b/src/lenz_io/cli/verify.py index f0ca5a5..8373375 100644 --- a/src/lenz_io/cli/verify.py +++ b/src/lenz_io/cli/verify.py @@ -198,7 +198,7 @@ def _poll( # Sub-claims inherit the parent submission's depth server-side # (/select reads it off the task meta), so nothing to send here. items = client.select(task_id, claims=texts).items - picks = [(it.task_id, it.claim or txt) for it, txt in zip(items, texts, strict=True)] + picks = [(it.task_id, it.claim_text or txt) for it, txt in zip(items, texts, strict=True)] # detach, or >1 claim → batch path; exactly one → keep the # single-verdict flow (nicer than a 1-row table). if detach or len(picks) > 1: @@ -256,7 +256,7 @@ def _resolve_multi_claim(out: Output, task_id: str, st: TaskStatus, selection: l Otherwise json mode emits the needs_input object and exits 3 (never hangs on a prompt that can't happen); a TTY shows the checkbox picker. """ - options = [c.claim for c in st.claims] + options = [c.text for c in st.claims] if selection is not None: return [options[i] for i in _selection_to_indices(selection, len(options))] if out.json_mode: @@ -393,8 +393,8 @@ def _emit_needs_input(out: Output, task_id: str, st: TaskStatus) -> None: "hint": st.hint, "task_id": task_id, "claims": [c.model_dump(mode="json") for c in st.claims], - # Deprecated compatibility keys, always empty; removal is planned - # for 2026-11-29 with the ``TaskStatus`` fields they mirrored. + # Deprecated compatibility keys, always empty; kept, like the + # ``TaskStatus`` fields they mirror. "candidates": [], "similar": [], } diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index b45141e..999648f 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -215,6 +215,18 @@ def _batch_item_body(item: Any) -> Any: return body +def _extracted(body: Any, *, locate: bool | None) -> ExtractedClaims: + """``/extract``'s answer. A newer-shape body (``claims``, no + ``identified_claims``) that located every claim away answers ``claims: + []``; the original field for that was ``locations=[]``, which only the + request (``locate=True``) can tell apart from "nothing found".""" + out = ExtractedClaims.model_validate(body) + newer = isinstance(body, dict) and "claims" in body and "identified_claims" not in body + if newer and locate and not body["claims"] and out.status == "not_a_claim" and out.locations is None: + out.locations = [] + return out + + #: An async job the poll loop waits on: a review or a citation check. _Job = TypeVar("_Job", ReviewFull, Citecheck) @@ -985,21 +997,19 @@ def verify_batch_and_wait( terminal, timed_out, gone = self._poll_to_terminal(ids, timeout, on_progress) results: list[BatchItemResult] = [] - # Built with ``claim_text`` so ``model_dump()`` keeps the keys it has - # always had; ``BatchItemResult.claim`` reads the same string. for it in accepted.items: # preserve input order status = terminal.get(it.task_id) if it.task_id in gone: # Removed by the account's retention period (410): terminal, # with no status to carry. - results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim, status="failed")) + results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="failed")) elif not it.task_id or it.task_id in timed_out or status is None: - results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim, status="timeout")) + results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="timeout")) elif status.status == "completed" and status.result is not None: results.append( BatchItemResult( task_id=it.task_id, - claim_text=it.claim, + claim_text=it.claim_text, status="completed", verification=status.result, status_detail=status, @@ -1007,12 +1017,14 @@ def verify_batch_and_wait( ) elif status.status == "needs_input": results.append( - BatchItemResult(task_id=it.task_id, claim_text=it.claim, status="needs_input", status_detail=status) + BatchItemResult( + task_id=it.task_id, claim_text=it.claim_text, status="needs_input", status_detail=status + ) ) else: # failed, or completed-without-result (treated as failed). results.append( - BatchItemResult(task_id=it.task_id, claim_text=it.claim, status="failed", status_detail=status) + BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="failed", status_detail=status) ) return results @@ -1528,8 +1540,6 @@ def _verification_from_terminal(self, status: TaskStatus, task_id: str) -> Verif ) # failed. Server sends the diagnostic under ``error``; fall back to the # legacy fields for resilience. - # ``failure.detail`` is the sentence in the newer response shape; - # ``error`` carries the original one (and is filled from the block). detail = status.error or status.failure_detail or status.failure_reason or "unknown" if status.retryable: fix = "Transient provider outage — retry the same request after a short wait." @@ -1668,7 +1678,7 @@ def _extract( if idempotency_key: headers["Idempotency-Key"] = idempotency_key body = self._request("POST", "/extract", json=payload, timeout=timeout, headers=headers) - return ExtractedClaims.model_validate(body) + return _extracted(body, locate=locate) def _assess( self, diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 47c9583..2f6579e 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -569,23 +569,29 @@ def map_response_to_error( # The generic 4xx advice ("retry; file an issue") is wrong for both: # the server's own hint says what to do, with a class default behind it. # A failed run states its cause flat in the original response shape - # and in one ``failure`` block in the newer one; read both, flat first. + # and in one ``failure`` block in the newer one. The block is read only + # for a newer-shape body, and only where the flat key is absent. raw_failure = parsed.get("failure") - failure: dict[str, Any] = raw_failure if isinstance(raw_failure, dict) else {} + newer = isinstance(raw_failure, dict) and "failure_reason" not in parsed + failure: dict[str, Any] = raw_failure if newer and isinstance(raw_failure, dict) else {} + + def _read(key: str, block_key: str) -> Any: + return parsed[key] if key in parsed else failure.get(block_key) + err.task_id = _opt_str(parsed.get("task_id")) - err.hint = _opt_str(parsed.get("hint")) or _opt_str(failure.get("hint")) + err.hint = _opt_str(_read("hint", "hint")) if isinstance(err, LenzVerificationNotReadyError): err.status = _opt_str(parsed.get("status")) err.fix = err.hint or "Wait for the run with client.wait(task_id), then read its result." else: - reason = _opt_str(parsed.get("failure_reason")) or _opt_str(failure.get("code")) + reason = _opt_str(_read("failure_reason", "code")) # The original spelling of "nothing checkable" on a verification. - err.failure_reason = "not_a_claim" if reason == "no_checkable_claim" else reason - err.failure_class = _opt_str(parsed.get("failure_class")) or _opt_str(failure.get("failure_class")) + err.failure_reason = "not_a_claim" if newer and reason == "no_checkable_claim" else reason + err.failure_class = _opt_str(_read("failure_class", "failure_class")) # Only a real boolean is a retry signal, as in the wait path. - retryable = parsed.get("retryable", failure.get("retryable")) + retryable = _read("retryable", "retryable") err.retryable = retryable if isinstance(retryable, bool) else None - err.doc_url = _opt_str(parsed.get("docs_url")) or _opt_str(failure.get("docs_url")) or err.doc_url + err.doc_url = _opt_str(_read("docs_url", "docs_url")) or err.doc_url if err.hint: err.fix = err.hint elif err.retryable: @@ -622,9 +628,9 @@ def map_response_to_error( # sending null precisely so "unknown" stays distinguishable from # "zero". Collapsing that here would throw the distinction away. err.remaining = _opt_int(parsed.get("remaining")) - if err.remaining is None and "remaining" not in parsed and "requested" not in parsed: - # A body without the capability figure (the newer response shape - # on some endpoints): the pool divided by this one call's price. + if "remaining" not in parsed and "requested" not in parsed and "doc_url" not in parsed and "docs_url" in parsed: + # A newer-shape body (``docs_url``) without the capability figure: + # the pool divided by this one call's price. pool, price = _opt_int(parsed.get("credits_remaining")), _opt_int(parsed.get("cost")) if pool is not None and price: err.remaining = pool // price @@ -649,9 +655,17 @@ def map_response_to_error( elif isinstance(err, LenzRateLimitError): err.limit = _opt_int(parsed.get("limit")) err.reset_in_seconds = _opt_int(parsed.get("reset_in_seconds")) - if err.reset_in_seconds is None and code not in _IN_FLIGHT_429_CODES: - # The newer response shape names every wait ``retry_after``. The - # in-flight refusals never carried ``reset_in_seconds``. + if ( + "reset_in_seconds" not in parsed + and "retry_after_seconds" not in parsed + and "retry_after" in parsed + and "docs_url" in parsed + and "doc_url" not in parsed + and code not in _IN_FLIGHT_429_CODES + ): + # A newer-shape body (``docs_url``) names every wait + # ``retry_after``. The in-flight refusals never carried + # ``reset_in_seconds``. err.reset_in_seconds = _opt_int(parsed.get("retry_after")) rl_upgrade_url = parsed.get("upgrade_url") err.upgrade_url = rl_upgrade_url if isinstance(rl_upgrade_url, str) else "" diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index d6146d9..c9fc1a9 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -12,68 +12,17 @@ - ``claim`` : str — the framed claim text - ``verdict`` : str — "True" | "Mostly True" | "Mixed" | "Mostly False" | "False" | "Error" - ("Error": an ``assess`` row with ``status == "failed"``) - ``confidence`` : str — "high" | "medium" | "low" (categorical) - ``lenz_score`` : int | None - 1-10 integer (deep / list; /assess omits) """ from __future__ import annotations -import functools -from collections.abc import Callable, ItemsView, KeysView, ValuesView +from collections.abc import ItemsView, KeysView, ValuesView from datetime import datetime, timezone from typing import Any, Literal -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PrivateAttr, - SerializerFunctionWrapHandler, - model_serializer, - model_validator, -) - -#: Where a validator lists the attributes it filled in that the response did -#: not carry. ``_Lax`` moves it to a private attribute; ``model_dump()`` leaves -#: those attributes out, so a dump is always the shape the server sent. -_DERIVED_KEY = "__lenz_derived__" - - -def _reads_both(*new_fields: str, always_hide: bool = False) -> Callable[[Callable[..., Any]], Callable[..., Any]]: - """Mark what a ``_read_both_shapes`` validator derived, so ``model_dump()`` - shows the shape the server sent. - - ``new_fields`` are the attributes this release added for the newer - response shape. Hidden from a dump: each of them the input did not carry - (so an original-shape response dumps exactly as it did before this - release), and, on a newer-shape input (one carrying any of them, or any - input with ``always_hide``), every attribute the input did not carry. A - value the validator rewrote (a failed row's ``verdict``) dumps as sent. - """ - - def wrap(fn: Callable[..., Any]) -> Callable[..., Any]: - @functools.wraps(fn) - def inner(cls: Any, data: Any) -> Any: - out = fn(cls, data) - if not isinstance(data, dict) or not isinstance(out, dict): - return out - hidden = {k for k in new_fields if k not in data} - if always_hide or any(k in data for k in new_fields): - hidden |= {k for k in out if k not in data} - hidden |= {k for k in cls.model_fields if k not in data} - rewritten = { - k: v - for k, v in data.items() - if k in out and not isinstance(v, (dict, list)) and out[k] is not v and out[k] != v - } - if not hidden and not rewritten: - return out - return {**out, _DERIVED_KEY: {"hidden": sorted(hidden), "sent": rewritten}} - - return inner - - return wrap +from pydantic import BaseModel, ConfigDict, Field, model_validator class _Lax(BaseModel): @@ -87,41 +36,25 @@ class _Lax(BaseModel): model_config = ConfigDict(extra="allow") - # The attributes filled in from the other response shape (see - # ``_reads_both``): readable, never dumped. - _lenz_derived: frozenset[str] = PrivateAttr(default_factory=frozenset) - _lenz_sent: dict[str, Any] = PrivateAttr(default_factory=dict) - - @model_validator(mode="after") - def _collect_derived(self) -> Any: - extra = self.__pydantic_extra__ - if extra and _DERIVED_KEY in extra: - marks = extra.pop(_DERIVED_KEY) - self._lenz_derived = frozenset(marks["hidden"]) - self._lenz_sent = dict(marks["sent"]) - return self - - @model_serializer(mode="wrap") - def _dump_as_sent(self, handler: SerializerFunctionWrapHandler) -> Any: - data = handler(self) - if isinstance(data, dict): - for key in self._lenz_derived: - data.pop(key, None) - for key, value in self._lenz_sent.items(): - if key in data: - data[key] = value - return data - # ── Reading both response shapes ───────────────────────────────────────── # # The API is gaining a second, dated response shape that renames a handful of # fields (``claim_text`` -> ``claim``, ``modified_at`` -> ``completed_at``, a # failed item's ``error`` / ``error_code`` / ``failure_reason`` -> one -# ``failure`` block, ...). This release still asks for the original shape, but -# every model reads either one: the new name is a first-class attribute, and -# every old attribute keeps its old meaning, computed from whichever shape -# arrived. The helpers below are the shared rules. +# ``failure`` block, ...). This release still asks for the original shape. +# +# Two rules keep existing code exactly as it was: +# +# * A body is treated as the newer shape only when it carries something only +# that shape has. Every other body is parsed exactly as before: same fields, +# same values, same dump, same ``exclude_unset``. +# * A newer-shape body gets the original fields filled in, with their original +# meaning, from the newer ones (only where the body does not carry them). +# +# The newer names are read-only properties, computed from either shape: they +# are not model fields, so ``repr``, ``model_dump()``, equality and pickling of +# an original-shape object are unchanged. #: The one code for "the input holds nothing that can be checked", in the #: newer response shape. The original shape spells it per endpoint: @@ -140,6 +73,12 @@ def _old_code(code: Any, old: str) -> Any: return old if code in (NO_CHECKABLE_CLAIM, *_OLD_NO_CLAIM_CODES) else code +def _is_newer(data: Any, has: str, lacks: str) -> bool: + """A dict carrying ``has`` (a key only the newer shape sends) and not + ``lacks`` (its original-shape counterpart).""" + return isinstance(data, dict) and has in data and lacks not in data + + def _utc_day(iso: Any) -> Any: """The UTC calendar day of an ISO-8601 string, or ``None``.""" if not isinstance(iso, str) or not iso: @@ -162,30 +101,24 @@ def _modified_at_from(created_at: Any, completed_at: Any) -> str | None: return str(completed_at) -def _with_completion_times(data: Any) -> Any: - """Fill ``completed_at`` and ``modified_at`` from whichever one arrived. - - ``modified_at`` from ``completed_at``: the original later-calendar-day - rule. ``completed_at`` from ``modified_at``: exact when ``modified_at`` is - set; when it is null (a same-day completion) the original shape does not - say when the verification completed, so ``completed_at`` stays ``None``. - """ - if not isinstance(data, dict): - return data - if "completed_at" in data and "modified_at" not in data: +def _fill_modified_at(data: Any) -> Any: + """A newer-shape verification (``completed_at``, no ``modified_at``): + add the original ``modified_at``.""" + if _is_newer(data, "completed_at", "modified_at"): return {**data, "modified_at": _modified_at_from(data.get("created_at"), data.get("completed_at"))} - if "modified_at" in data and "completed_at" not in data: - return {**data, "completed_at": data.get("modified_at")} return data -def _mirror(data: dict[str, Any], new: str, old: str) -> dict[str, Any]: - """Copy a field across a plain rename, in whichever direction is missing.""" - if new in data and old not in data: - return {**data, old: data[new]} - if old in data and new not in data: - return {**data, new: data[old]} - return data +def _fill(data: dict[str, Any], **values: Any) -> dict[str, Any]: + """``data`` with each of ``values`` added where the key is missing.""" + return {**{k: v for k, v in values.items() if k not in data}, **data} + + +def _sent(model: BaseModel, key: str) -> Any: + """The value the server sent under ``key`` when it is not a field of this + release's model (a newer-shape key), else ``None``.""" + extra = model.__pydantic_extra__ or {} + return extra.get(key) #: Closed set of failure causes on a ``failed`` verification, mirroring the @@ -264,22 +197,23 @@ class Audit(_Lax): class CandidateClaim(_Lax): - """One of multiple distinct claims framing found in the submitted text. - - ``claim`` is the claim's text. ``text`` is its older name, the same - string, kept for existing code (deprecated; either shape fills both). - """ + """One of multiple distinct claims framing found in the submitted text.""" - claim: str = "" - #: Deprecated: the same string as :attr:`claim`. - text: str = Field(default="", json_schema_extra={"deprecated": True}) + text: str = "" domain: str = "" @model_validator(mode="before") @classmethod - @_reads_both("claim") - def _read_both_shapes(cls, data: Any) -> Any: - return _mirror(data, "claim", "text") if isinstance(data, dict) else data + def _read_newer_shape(cls, data: Any) -> Any: + if _is_newer(data, "claim", "text"): + return _fill(data, text=data["claim"]) + return data + + @property + def claim(self) -> str: + """The option's claim (``text`` is its older name, the same string).""" + sent = _sent(self, "claim") + return sent if isinstance(sent, str) else self.text class EntityRef(_Lax): @@ -400,9 +334,8 @@ class Verification(_Lax): endpoint, and the webhook payload. The verdict block is FLAT at top level (was nested ``Verdict`` object - pre-unify). ``created_at`` and ``completed_at`` are its timestamps - (``modified_at`` is the deprecated older one) — editorial - ``published_at`` is internal-only. + pre-unify). ``created_at`` + ``modified_at`` are the only timestamp + fields on the API surface — editorial ``published_at`` is internal-only. 1.1.0: dropped ``url`` and ``visibility``. API claims are private by default and referenced by ``verification_id`` only. Cache-hit on @@ -451,13 +384,7 @@ class Verification(_Lax): sources: list[Source] = Field(default_factory=list) audit: Audit = Field(default_factory=Audit) created_at: str | None = None - #: When the verification completed. In the original response shape it is - #: known only through ``modified_at``: set when that is, ``None`` on a - #: same-day completion (and on servers that send neither). - completed_at: str | None = None - #: Deprecated: the completion time when it falls on a later UTC calendar - #: day than ``created_at``, else ``None``. Read :attr:`completed_at`. - modified_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + modified_at: str | None = None # Output language (ISO 639-1). Always populated by the server when # the SDK is fresh; defaulted to ``'en'`` for resilience against # older cached payloads that lack the field. @@ -474,9 +401,16 @@ class Verification(_Lax): @model_validator(mode="before") @classmethod - @_reads_both("completed_at") - def _read_both_shapes(cls, data: Any) -> Any: - return _with_completion_times(data) + def _read_newer_shape(cls, data: Any) -> Any: + return _fill_modified_at(data) + + @property + def completed_at(self) -> str | None: + """When the verification completed. From the original response shape it + is known only through ``modified_at``: set when that is, ``None`` on a + same-day completion.""" + sent = _sent(self, "completed_at") + return sent if isinstance(sent, str) else self.modified_at class VerificationListItem(_Lax): @@ -498,18 +432,22 @@ class VerificationListItem(_Lax): # A suggested rewrite of ``claim``. See ``Verification.suggested_rewrite``. suggested_rewrite: str | None = None created_at: str | None = None - #: See ``Verification.completed_at``. - completed_at: str | None = None - #: Deprecated. See ``Verification.modified_at``. - modified_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + modified_at: str | None = None # Output language (ISO 639-1). See ``Verification.language``. language: str = "en" @model_validator(mode="before") @classmethod - @_reads_both("completed_at") - def _read_both_shapes(cls, data: Any) -> Any: - return _with_completion_times(data) + def _read_newer_shape(cls, data: Any) -> Any: + return _fill_modified_at(data) + + @property + def completed_at(self) -> str | None: + """When the verification completed. From the original response shape it + is known only through ``modified_at``: set when that is, ``None`` on a + same-day completion.""" + sent = _sent(self, "completed_at") + return sent if isinstance(sent, str) else self.modified_at class VerificationList(_Lax): @@ -582,9 +520,7 @@ class ClaimLocation(_Lax): #: from lenz_io import ExtractStatus #: #: - ``ready`` — claims were found (and, with a ``focus``, at least one matched). -#: - ``not_a_claim`` — the text holds no verifiable factual claim at all (the -#: newer response shape spells it ``no_checkable_claim``; this release -#: reads both as ``not_a_claim``). +#: - ``not_a_claim`` — the text holds no verifiable factual claim at all. #: - ``no_match`` — claims were found, but none fall within the ``focus``. #: #: A DOCUMENTATION constant, not a guarantee. The model field stays ``str``, @@ -606,37 +542,30 @@ class ClaimLocation(_Lax): class ExtractedClaim(ClaimLocation): """One claim ``/extract`` found: ``claim``, and with ``locate=True`` its ``positions`` in the text (``None`` when the call did not locate, or the - claims could not be located).""" + claims could not be located). Returned by ``ExtractedClaims.claims``.""" class ExtractedClaims(_Lax): """Output of ``POST /extract``. - ``claims`` is every claim found, most check-worthy first, always a list - (one entry for one claim, ``[]`` for none). With ``locate=True`` each - entry's ``positions`` says where the text makes it. - ``status`` is one of ``ExtractStatus``: ``ready``, ``not_a_claim``, or — when a ``focus`` was given and no claim fell within it — ``no_match``. - ``no_match`` is a successful answer, not an error: ``claims`` is empty - and the unfocused list is never substituted for it. In this release - ``status`` keeps the ``not_a_claim`` spelling whichever response shape - arrived (the newer shape spells it ``no_checkable_claim``). - - Deprecated, kept with their original meaning: ``claim`` is the first - claim (``""`` for none), ``identified_claims`` the whole list when there - are two or more (``[]`` for one), and ``locations`` the ``ClaimLocation`` - list of a located call (``None`` when ``locate`` was not set, when the - extraction found no claims, or when the claims could not be located). - Read ``claims`` instead: it is filled from either response shape. + ``no_match`` is a successful answer, not an error: ``identified_claims`` + is empty and the unfocused list is never substituted for it. + + ``locations`` is set only on a call made with ``locate=True``: one + ``ClaimLocation`` per returned claim, in the order of + ``identified_claims`` (one entry for a single ``claim``). It is ``[]`` + when every claim was left out (``status`` is then ``"not_a_claim"``), + and ``None`` when ``locate`` was not set, when the extraction found no + claims, or when the + claims could not be located (the list is then returned unfiltered). + Older servers omit the key; it parses as ``None``. """ status: str = "" - claims: list[ExtractedClaim] = Field(default_factory=list) - #: Deprecated: the first of :attr:`claims`. - claim: str = Field(default="", json_schema_extra={"deprecated": True}) - #: Deprecated: :attr:`claims` when there are two or more, else ``[]``. - identified_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) + claim: str = "" + identified_claims: list[str] = Field(default_factory=list) # Deprecated: always empty since 2026-09-12. Kept because the server # still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) @@ -644,84 +573,43 @@ class ExtractedClaims(_Lax): key_entities: list[ExtractedEntity] = Field(default_factory=list) presumed_intent: str = "" original_input: str = "" - #: Deprecated: the ``positions`` of :attr:`claims`, as one - #: ``ClaimLocation`` per claim. - locations: list[ClaimLocation] | None = Field(default=None, json_schema_extra={"deprecated": True}) + locations: list[ClaimLocation] | None = None @model_validator(mode="before") @classmethod - @_reads_both("claims") - def _read_both_shapes(cls, data: Any) -> Any: - if not isinstance(data, dict): + def _read_newer_shape(cls, data: Any) -> Any: + if not (_is_newer(data, "claims", "identified_claims") and isinstance(data["claims"], list)): return data - out = dict(data) + items = [c for c in data["claims"] if isinstance(c, dict)] + names = [c.get("claim") if isinstance(c.get("claim"), str) else "" for c in items] + located = bool(items) and all(c.get("positions") is not None for c in items) + out = _fill( + data, + claim=names[0] if names else "", + identified_claims=names if len(names) > 1 else [], + locations=[{"claim": n, "positions": c.get("positions")} for n, c in zip(names, items, strict=True)] + if located + else None, + ) if out.get("status") == NO_CHECKABLE_CLAIM: out["status"] = "not_a_claim" - claims = out.get("claims") - if isinstance(claims, list): - # The newer shape: derive the three original fields. - items = [c for c in claims if isinstance(c, dict)] - names = [str(c.get("claim") or "") for c in items] - out.setdefault("claim", names[0] if names else "") - out.setdefault("identified_claims", names if len(names) > 1 else []) - if "locations" not in out: - located = bool(items) and all(c.get("positions") is not None for c in items) - out["locations"] = ( - [{"claim": c.get("claim") or "", "positions": c.get("positions")} for c in items] - if located - else None - ) - return out - # The original shape: build ``claims`` from ``claim`` + ``identified_claims``. - identified = [c for c in out.get("identified_claims") or [] if isinstance(c, str)] - primary = out.get("claim") if isinstance(out.get("claim"), str) else "" - # ``identified_claims`` normally starts with ``claim``; an older server - # sent the extras only, so take the union, ``claim`` first. - names = ([primary] if primary else []) + [c for c in identified if c != primary] - positions: dict[str, Any] = {} - for loc in out.get("locations") or []: - if isinstance(loc, dict) and isinstance(loc.get("claim"), str): - positions.setdefault(loc["claim"], loc.get("positions")) - out["claims"] = [{"claim": n, "positions": positions.get(n)} for n in names] return out - -class FailureBlock(_Lax): - """Why something failed: a review, one claim's work inside it, a citation - check, an ``assess`` row or a verification. - - ``code`` is the specific cause (an open set, e.g. ``no_checkable_claim``, - ``insufficient_credits``, ``timeout``), ``detail`` one sentence on what - happened (``None`` from the original response shape, which has no such - sentence here), ``failure_class`` the closed :data:`FailureClass`, - ``retryable`` whether resending the same request can help, ``hint`` one - sentence on what to do next and ``docs_url`` where the class is explained. - - ``failure_reason`` is the older name of ``code`` (deprecated), with its - original spelling: ``no_claim`` where ``code`` reads - ``no_checkable_claim``. Either response shape fills both. - """ - - code: str | None = "" - detail: str | None = None - #: Deprecated: :attr:`code`, spelling "nothing checkable" ``no_claim``. - failure_reason: str | None = Field(default="", json_schema_extra={"deprecated": True}) - failure_class: str | None = "" - retryable: bool | None = None - hint: str | None = None - docs_url: str | None = "" - - @model_validator(mode="before") - @classmethod - @_reads_both("code", "detail") - def _read_both_shapes(cls, data: Any) -> Any: - if not isinstance(data, dict): - return data - if "code" in data and "failure_reason" not in data: - return {**data, "failure_reason": _old_code(data["code"], "no_claim")} - if "failure_reason" in data and "code" not in data: - return {**data, "code": _new_code(data["failure_reason"])} - return data + @property + def claims(self) -> list[ExtractedClaim]: + """Every claim found, most check-worthy first: always a list (one entry + for one claim, ``[]`` for none), each with its ``positions`` when the + call located them. Read from either response shape.""" + sent = _sent(self, "claims") + if isinstance(sent, list): + return [ExtractedClaim.model_validate(c) for c in sent if isinstance(c, dict)] + names = list(self.identified_claims) or ([self.claim] if self.claim else []) + if self.claim and self.claim not in names: + names.insert(0, self.claim) + positions: dict[str, Any] = {} + for loc in self.locations or []: + positions.setdefault(loc.claim, loc.positions) + return [ExtractedClaim(claim=n, positions=positions.get(n)) for n in names] class AssessClaim(_Lax): @@ -732,38 +620,29 @@ class AssessClaim(_Lax): ``ClaimDetailOut`` payload at ``GET /api/v1/verifications/{id}`` for callers that want citations and the full audit trail. - ``status`` is ``"completed"`` for a row with a verdict and ``"failed"`` - for one that could not be given a verdict. A failed row stays in - position (one row per item sent on a list call), is not charged, and - says why in ``failure``: ``failure.code`` names the cause and - ``failure.hint`` is one sentence on what to send next — the field to - surface to a human, written per cause. A vague input is assessed on its - most likely reading, which ``claim`` carries. A compound input is - assessed on its main claim; the other claims found in it are listed in - ``more_claims``. - - The original fields keep their meaning whichever response shape - arrived: a failed row reads ``verdict == "Error"`` and - ``confidence == "low"``, its cause is in ``error_code`` (``no_claim`` - where ``failure.code`` reads ``no_checkable_claim``) and its next step in - ``hint``; ``identified_claims`` is ``more_claims``. These fields default - empty so older servers that don't send them still parse. + A row with ``verdict == "Error"`` could not be given a verdict. On a + list call it stays in position (one row per item sent), is not charged, + and says why: ``error_code`` names the cause and ``hint`` is one + sentence on what to send next. ``hint`` is the field to surface to a + human — it is written per cause and stays correct as causes are added. + A vague input is assessed on its most likely reading, which ``claim`` + carries. A compound input is assessed on its main claim; the other + claims found in it are listed in ``identified_claims`` (also with a + ``hint``). These fields default empty so older servers that don't send + them still parse. """ claim: str = "" # Output language (ISO 639-1). Echoes the language requested on the # call, or ``'en'`` when unspecified. Verdict enums always English. language: str = "en" - #: ``"completed"`` (a verdict) or ``"failed"`` (none; see ``failure``). - #: ``""`` only from a server that sent neither a status nor a verdict. - status: str = "" verdict: str = "" # "True" | "Mostly True" | "Mixed" | "Mostly False" | "False" | "Error" confidence: str = "low" # "high" | "medium" | "low" verification_url: str | None = None # ``rationale`` is the reasoning of a reviewer who agrees with the panel's # verdict; ``dissent``, when set, is the reasoning of the reviewer farthest # from it. Both are reviewers' notes, not checked sources; for sourced - # evidence, call ``verify``. Read both as optional: a failed row has + # evidence, call ``verify``. Read both as optional: an ``"Error"`` row has # neither, and a response replayed from before the API added them carries # neither key. rationale: str | None = None @@ -776,69 +655,80 @@ class AssessClaim(_Lax): # check's reasoning and not itself verified: review it, or run it through # ``verify``, before using it. suggested_rewrite: str | None = None - #: Why a failed row has no verdict; ``None`` on a completed row. - #: ``failure.code`` is an OPEN set (``no_checkable_claim`` | - #: ``framing_failed`` | ``upstream_unavailable`` | ``timeout`` today): - #: branch on the ones you know and fall through on the rest. - #: - #: Which are worth resending as-is: ``upstream_unavailable`` (a provider - #: was down) and ``timeout`` (the call ran out of its time budget before - #: this item was done — send fewer items per call to make it less likely). - #: ``framing_failed`` is deterministic, so retrying the same text will not - #: help. ``no_checkable_claim`` wants a different input; read - #: ``failure.hint``. - failure: FailureBlock | None = None - #: Other claims found in the input that were NOT assessed; else empty. - more_claims: list[str] = Field(default_factory=list) - #: Deprecated: ``failure.code`` on a failed row, with "nothing - #: checkable" spelled ``no_claim``; ``None`` on a completed row. - error_code: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + # Only on ``verdict == "Error"`` rows: 'no_claim' | 'framing_failed' | + # 'upstream_unavailable' | 'timeout'. + # + # An OPEN set, deliberately typed ``str`` rather than a Literal: the API + # may add a cause in a minor version, so branch on the ones you know and + # fall through on the rest. + # + # Which are worth resending as-is: 'upstream_unavailable' (a provider was + # down) and 'timeout' (the call ran out of its time budget before this + # item was done — send fewer items per call to make it less likely). + # 'framing_failed' is deterministic, so retrying the same text will not + # help (a provider outage comes back as 'upstream_unavailable' instead). + # 'no_claim' wants a different input; read ``hint``. + error_code: str | None = None # Deprecated: always empty since 2026-09-12, when the ``ambiguous`` cause # that filled it was retired. Kept because the server still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) - #: Deprecated: :attr:`more_claims`. - identified_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) - #: Deprecated: one sentence on what to send next. On a failed row it is - #: ``failure.hint``. The original response shape also set it on a row - #: with other claims found; the newer shape does not. - hint: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + # Other claims found in the input that were NOT assessed; else empty. + identified_claims: list[str] = Field(default_factory=list) + # One sentence on what to send next. Set on every Error row and on a row + # with a non-empty ``identified_claims``; ``None`` on a plain verdict row. + hint: str | None = None @model_validator(mode="before") @classmethod - @_reads_both("status", "failure", "more_claims") - def _read_both_shapes(cls, data: Any) -> Any: - if not isinstance(data, dict): + def _read_newer_shape(cls, data: Any) -> Any: + if not _is_newer(data, "failure", "error_code"): return data - out = _mirror(dict(data), "more_claims", "identified_claims") - status = out.get("status") - verdict = out.get("verdict") - if not status: - status = "failed" if verdict == "Error" else ("completed" if verdict else "") - out["status"] = status - if status == "failed": - # The original meaning of a row with no verdict. - if not verdict: - out["verdict"] = "Error" - if out.get("confidence") is None: - out["confidence"] = "low" - else: - if verdict is None: - out["verdict"] = "" - if out.get("confidence") is None: - out["confidence"] = "low" - failure = out.get("failure") - if isinstance(failure, dict): - code = failure.get("code", failure.get("failure_reason")) - out.setdefault("error_code", _old_code(code, "no_claim")) - out.setdefault("hint", failure.get("hint")) - elif failure is None and status == "failed" and "failure" not in data: - # The original shape: build the block from the row's own fields. - out["failure"] = {"code": _new_code(out.get("error_code") or ""), "hint": out.get("hint")} - return out + out = dict(data) + failed = out.get("status") == "failed" + if out.get("verdict") is None: + out["verdict"] = "Error" if failed else "" + if out.get("confidence") is None: + out["confidence"] = "low" + failure = out.get("failure") if isinstance(out.get("failure"), dict) else {} + return _fill( + out, + error_code=_old_code(failure.get("code"), "no_claim") if failure else None, + hint=failure.get("hint") if failure else None, + identified_claims=out.get("more_claims") or [], + ) + + @property + def status(self) -> str: + """``"completed"`` (a verdict) or ``"failed"`` (none; see ``failure``).""" + sent = _sent(self, "status") + if isinstance(sent, str): + return sent + if self.verdict == "Error": + return "failed" + return "completed" if self.verdict else "" + + @property + def failure(self) -> FailureBlock | None: + """Why a failed row has no verdict (``None`` on a completed row): + ``code`` (``no_checkable_claim`` | ``framing_failed`` | + ``upstream_unavailable`` | ``timeout`` today, an open set) and ``hint``, + one sentence on what to send next.""" + sent = _sent(self, "failure") + if isinstance(sent, dict): + return FailureBlock.model_validate(sent) + if self.status != "failed": + return None + return FailureBlock(failure_reason=self.error_code or "", hint=self.hint) + + @property + def more_claims(self) -> list[str]: + """Other claims found in the input that were not assessed.""" + sent = _sent(self, "more_claims") + return list(sent) if isinstance(sent, list) else list(self.identified_claims) #: The ``AssessResponse.status`` values this release knows about. A -#: documentation constant, like ``ExtractStatus``: the field stays ``str``. +#: documentation constant, like ``ExtractStatus``: the attribute is a ``str``. #: #: - ``ok`` — at least one row has a verdict (a list may mix in failed rows). #: - ``no_checkable_claim`` — the input, or every item, holds nothing that @@ -854,32 +744,23 @@ class AssessResponse(_Lax): found in the input — up to 20, at 1 credit each. A text that makes more claims than one call checks gets its most check-worthy 20 checked and the rest listed in ``more_claims``, unchecked and free: send them back with - ``assess(claims=...)``, 20 a call, to check them. When the input holds no - checkable claim, ``claims`` is empty, ``status`` is - ``"no_checkable_claim"`` and ``failure`` says why. + ``assess(claims=...)``, 20 a call, to check them. ``error`` is set when + the input holds no checkable claim. List form (``assess(claims=[...])``): exactly one entry per item sent, - in the order sent. An item that could not be given a verdict is a - ``"failed"`` row in position (see ``AssessClaim``), never a missing one; - the top-level ``failure`` stays ``None``. - - ``status`` (see ``AssessStatus``) sums the call up: ``ok``, - ``no_checkable_claim`` or ``error``. Computed from the rows when a server - does not send it. - - Deprecated, kept with their original meaning: ``error`` is the sentence - (``failure.detail``) and ``error_code`` is ``'no_claim'`` when the - single form found no checkable claim; both are empty otherwise. + in the order sent. An item that could not be given a verdict is an + ``"Error"`` row in position (see ``AssessClaim``), never a missing one; + the top-level ``error`` stays ``None``. + + When ``claims`` is empty (single form), ``error_code`` is ``'no_claim'``: + the input holds no checkable claim (a vague input is assessed on its + most likely reading instead). It defaults empty, so older servers that + don't send it degrade to the plain ``error`` message. """ - status: str = "" claims: list[AssessClaim] = Field(default_factory=list) - #: Why the single form has no rows; ``None`` otherwise. - failure: FailureBlock | None = None - #: Deprecated: ``failure.detail``. - error: str | None = Field(default=None, json_schema_extra={"deprecated": True}) - #: Deprecated: ``'no_claim'`` when ``failure`` says nothing was checkable. - error_code: str = Field(default="", json_schema_extra={"deprecated": True}) # '' | 'no_claim' + error: str | None = None + error_code: str = "" # '' | 'no_claim' # Deprecated: always empty since 2026-09-12. Kept because the server # still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) @@ -889,63 +770,64 @@ class AssessResponse(_Lax): @model_validator(mode="before") @classmethod - @_reads_both("status", "failure") - def _read_both_shapes(cls, data: Any) -> Any: - if not isinstance(data, dict): + def _read_newer_shape(cls, data: Any) -> Any: + if not _is_newer(data, "failure", "error"): return data - out = dict(data) - failure = out.get("failure") - if isinstance(failure, dict): - code = failure.get("code", failure.get("failure_reason")) - out.setdefault("error", failure.get("detail")) - out.setdefault("error_code", _old_code(code, "no_claim") or "") - elif failure is None and "failure" not in data and (out.get("error") or out.get("error_code")): - out["failure"] = {"code": _new_code(out.get("error_code") or ""), "detail": out.get("error")} - if out.get("error_code") is None: - out["error_code"] = "" - if not out.get("status"): - out["status"] = _assess_status(out) - return out - - -def _assess_status(body: dict[str, Any]) -> str: - """``AssessResponse.status`` for a body that does not carry it.""" - rows = [r for r in body.get("claims") or [] if isinstance(r, dict)] - failed = [r for r in rows if r.get("status") == "failed" or r.get("verdict") in ("Error", None, "")] - if rows and len(failed) < len(rows): - return "ok" - - def _code(block: Any, fallback: Any) -> Any: - if isinstance(block, dict): - return block.get("code", block.get("failure_reason")) - return fallback + failure = data.get("failure") if isinstance(data.get("failure"), dict) else None + if failure is None: + return data + return _fill( + data, + error=failure.get("detail"), + error_code=_old_code(failure.get("code"), "no_claim") or "", + ) - if not rows: - code = _code(body.get("failure"), body.get("error_code")) - return NO_CHECKABLE_CLAIM if _new_code(code) == NO_CHECKABLE_CLAIM else "error" - codes = {_new_code(_code(r.get("failure"), r.get("error_code"))) for r in rows} - return NO_CHECKABLE_CLAIM if codes == {NO_CHECKABLE_CLAIM} else "error" + @property + def status(self) -> str: + """``ok`` (some row has a verdict), ``no_checkable_claim`` (the input, + or every item, holds nothing checkable) or ``error``. See + ``AssessStatus``; computed from the rows when a server does not send it.""" + sent = _sent(self, "status") + if isinstance(sent, str): + return sent + rows = self.claims + if any(r.status == "completed" for r in rows): + return "ok" + if not rows: + return NO_CHECKABLE_CLAIM if _new_code(self.error_code) == NO_CHECKABLE_CLAIM else "error" + codes = {r.failure.code if r.failure else None for r in rows} + return NO_CHECKABLE_CLAIM if codes == {NO_CHECKABLE_CLAIM} else "error" + + @property + def failure(self) -> FailureBlock | None: + """Why the single form has no rows; ``None`` otherwise.""" + sent = _sent(self, "failure") + if isinstance(sent, dict): + return FailureBlock.model_validate(sent) + if not (self.error or self.error_code): + return None + return FailureBlock.model_validate({"failure_reason": self.error_code, "detail": self.error}) class TaskAccepted(_Lax): - """Returned by ``POST /verify`` and per item of ``POST /verify/batch`` - and ``POST /verify/{task_id}/select``. - - ``claim`` is the item's claim on a batch or select receipt (``""`` on a - single ``verify`` receipt). ``claim_text`` is its older name, the same - string (deprecated; either response shape fills both). - """ + """Returned by ``POST /verify`` and per item of ``POST /verify/batch``.""" task_id: str = "" - claim: str = "" - #: Deprecated: the same string as :attr:`claim`. - claim_text: str = Field(default="", json_schema_extra={"deprecated": True}) + claim_text: str = "" @model_validator(mode="before") @classmethod - @_reads_both("claim") - def _read_both_shapes(cls, data: Any) -> Any: - return _mirror(data, "claim", "claim_text") if isinstance(data, dict) else data + def _read_newer_shape(cls, data: Any) -> Any: + if _is_newer(data, "claim", "claim_text"): + return _fill(data, claim_text=data["claim"]) + return data + + @property + def claim(self) -> str: + """The item's claim on a batch or select receipt (``""`` on a single + ``verify`` receipt); ``claim_text`` is its older name.""" + sent = _sent(self, "claim") + return sent if isinstance(sent, str) else self.claim_text class BatchAccepted(_Lax): @@ -1035,27 +917,18 @@ class TaskStatus(_Lax): claims: list[CandidateClaim] = Field(default_factory=list) # Deprecated, both always empty: the API no longer sends them, and # ``reason`` is only ever ``multi_claim``. Kept so code that reads them - # keeps working; removal is planned for 2026-11-29. Marked deprecated in + # keeps working; they are deprecated and kept. Marked deprecated in # the JSON schema only, so reading them does not warn. candidates: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) similar_claims: list[SimilarVerification] = Field(default_factory=list, json_schema_extra={"deprecated": True}) - #: On ``failed``: why. ``failure.code`` is the specific cause (an open - #: set, e.g. ``research_empty``, ``no_checkable_claim``), ``failure.detail`` - #: one sentence on what happened, ``failure.failure_class`` the closed - #: :data:`FailureClass`, ``failure.retryable`` whether resending the same - #: claim can help, ``failure.hint`` what to send instead (when the server - #: has one) and ``failure.docs_url`` where the class is explained. Filled - #: from either response shape; ``None`` on every other status. - failure: FailureBlock | None = None - # The original failure fields, deprecated and kept with their original - # meaning whichever response shape arrived: ``error`` is the sentence, - # ``failure_reason`` the cause (``not_a_claim`` where ``failure.code`` - # reads ``no_checkable_claim``), and ``failure_class`` / ``retryable`` / - # ``docs_url`` / ``hint`` the same values as on ``failure``. - # ``failure_detail`` is never sent by the API; it stays for old code. - error: str = Field(default="", json_schema_extra={"deprecated": True}) - failure_reason: str = Field(default="", json_schema_extra={"deprecated": True}) - failure_detail: str = Field(default="", json_schema_extra={"deprecated": True}) + # failure branches. The server's failed response is + # ``{"status": "failed", "error": "..."}`` — ``error`` is the live wire + # field. ``failure_reason`` / ``failure_detail`` are kept for forward/back + # compatibility and other channels; read precedence is + # ``error or failure_detail or failure_reason``. + error: str = "" + failure_reason: str = "" + failure_detail: str = "" # WHY it failed — the closed set is ``FailureClass`` (import it for # exhaustive matching). The annotation stays ``str`` on purpose: a # ``Literal`` here would make an unknown class the server adds later a @@ -1069,40 +942,42 @@ class TaskStatus(_Lax): @model_validator(mode="before") @classmethod - @_reads_both("failure") - def _read_both_shapes(cls, data: Any) -> Any: - if not isinstance(data, dict): + def _read_newer_shape(cls, data: Any) -> Any: + if not _is_newer(data, "failure", "failure_reason") or not isinstance(data["failure"], dict): return data - out = dict(data) - failure = out.get("failure") - if isinstance(failure, dict): - # The newer shape: the original top-level fields from the block. - code = failure.get("code", failure.get("failure_reason")) - for key, value in ( - ("error", failure.get("detail")), - ("failure_reason", _old_code(code, "not_a_claim")), - ("failure_class", failure.get("failure_class")), - ("retryable", failure.get("retryable")), - ("docs_url", failure.get("docs_url")), - ("hint", failure.get("hint")), - ): - if out.get(key) is None: - out[key] = value - elif out.get("status") == "failed" and failure is None: - # The original shape: the block from the top-level fields. - out["failure"] = { - "code": _new_code(out.get("failure_reason") or ""), - "detail": out.get("error") or out.get("failure_detail") or None, - "failure_class": out.get("failure_class") or "", - "retryable": out.get("retryable") if isinstance(out.get("retryable"), bool) else None, - "hint": out.get("hint") or None, - "docs_url": out.get("docs_url") or "", + failure = data["failure"] + values = { + "error": failure.get("detail"), + "failure_reason": _old_code(failure.get("code"), "not_a_claim"), + "failure_class": failure.get("failure_class"), + "retryable": failure.get("retryable"), + "docs_url": failure.get("docs_url"), + "hint": failure.get("hint"), + } + # ``None`` reads as the original field's empty value. + return _fill(data, **{k: ("" if v is None and k != "retryable" else v) for k, v in values.items()}) + + @property + def failure(self) -> FailureBlock | None: + """On ``failed``: why. ``code`` is the cause (an open set, e.g. + ``research_empty``, ``no_checkable_claim``), ``detail`` one sentence on + what happened, ``failure_class`` / ``retryable`` / ``hint`` / + ``docs_url`` as on the original fields. ``None`` on other statuses.""" + sent = _sent(self, "failure") + if isinstance(sent, dict): + return FailureBlock.model_validate(sent) + if self.status != "failed": + return None + return FailureBlock.model_validate( + { + "failure_reason": self.failure_reason, + "detail": self.error or self.failure_detail or None, + "failure_class": self.failure_class, + "retryable": self.retryable, + "hint": self.hint or None, + "docs_url": self.docs_url, } - # ``None`` reads as the field's empty value, never as a validation error. - for key in ("error", "failure_reason", "failure_detail", "failure_class", "docs_url", "hint", "reason"): - if key in out and out[key] is None: - out[key] = "" - return out + ) class BatchItemResult(_Lax): @@ -1122,18 +997,15 @@ class BatchItemResult(_Lax): """ task_id: str = "" - claim: str = "" - #: Deprecated: the same string as :attr:`claim`. - claim_text: str = Field(default="", json_schema_extra={"deprecated": True}) + claim_text: str = "" status: Literal["completed", "needs_input", "failed", "timeout"] verification: Verification | None = None status_detail: TaskStatus | None = None - @model_validator(mode="before") - @classmethod - @_reads_both("claim") - def _read_both_names(cls, data: Any) -> Any: - return _mirror(data, "claim", "claim_text") if isinstance(data, dict) else data + @property + def claim(self) -> str: + """The item's claim (``claim_text`` is its older name).""" + return self.claim_text class UsageCredits(_Lax): @@ -1161,8 +1033,7 @@ class UsageCredits(_Lax): extra: int = 0 #: **Deprecated** old name of :attr:`extra`, the same number, kept for #: existing code. Reading it emits a ``DeprecationWarning``; it stays in - #: ``model_dump()`` output (unwarned), filled from ``extra`` when the - #: server does not send it. + #: ``model_dump()`` output (unwarned) for as long as the server sends it. bonus: int = Field( default=0, deprecated=( @@ -1177,8 +1048,8 @@ def _mirror_extra_and_bonus(cls, data: Any) -> Any: """Keep ``extra`` and its deprecated old name in step, in both directions. A server that has not started sending ``extra`` sends only ``bonus``; - the newer response shape sends only ``extra``. Mirroring here means - both attributes read correctly either way. + the newer response shape sends only ``extra``. Mirroring + here means both attributes read correctly either way. """ if not isinstance(data, dict): return data @@ -1194,10 +1065,9 @@ class UsageCapacity(_Lax): """DEPRECATED. One capability's share of the pool, in that capability's unit. Deprecated, together with the per-block ``credits`` alias, and kept for - existing code: the newer response shape does not send these blocks, and - the SDK then computes them from :attr:`Usage.credits` and - :attr:`Usage.costs`. Read those two and do the division yourself — it is - the same one this block does:: + existing code (computed from the pool when a response leaves the block + out). Read :attr:`Usage.credits` and :attr:`Usage.costs` and do the division — + it is the same one this block does:: remaining = u.credits.remaining // u.costs[capability] total = u.credits.total // u.costs[capability] @@ -1250,8 +1120,8 @@ class UsageCapacity(_Lax): def _mirror_bonus_and_credits(cls, data: Any) -> Any: """Keep ``bonus`` and its deprecated alias in step, in both directions. - A server predating the credit pool sends only ``credits``; a block the - SDK computed has only ``bonus``. Mirroring here means + A server predating the credit pool sends only ``credits``; a block + computed from the pool has only ``bonus``. Mirroring here means both attributes read correctly either way, so the SDK never depends on which side of an API deploy it is talking to. """ @@ -1302,9 +1172,7 @@ class Usage(_Lax): #: comparing against it will break on a rename that ought to be free. #: Empty on servers predating this field — fall back to :attr:`plan`. plan_label: str = "" - #: Deprecated: the same time as ``credits.resets_at``, which it is - #: filled from when the server does not send it. - quota_resets_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + quota_resets_at: str | None = None #: The credit balance — the authoritative number. Empty on older servers. credits: UsageCredits = Field(default_factory=UsageCredits) #: Credits per call, keyed by CAPABILITY, at its default price: @@ -1344,9 +1212,6 @@ class Usage(_Lax): #: :attr:`costs` instead:: #: #: left = u.credits.remaining // u.costs["verify"] - #: - #: Computed that way when the server does not send the block (the newer - #: response shape does not). verify: UsageCapacity = Field(default_factory=UsageCapacity) #: DEPRECATED, kept for existing code. See :attr:`verify`. ask: UsageCapacity = Field(default_factory=UsageCapacity) @@ -1362,45 +1227,35 @@ class Usage(_Lax): @model_validator(mode="before") @classmethod - @_reads_both(always_hide=True) - def _read_both_shapes(cls, data: Any) -> Any: - """Compute the deprecated blocks the newer response shape leaves out. - - Each per-capability block is the credit pool divided by that - capability's price, exactly as the server computed it; it is filled - only when the server sent the pool and the price but not the block. - """ - if not isinstance(data, dict): + def _read_newer_shape(cls, data: Any) -> Any: + """The newer shape sends the pool and the prices but not the + deprecated per-capability blocks: compute them exactly as the server + did, and ``quota_resets_at`` from ``credits.resets_at``.""" + if not (isinstance(data, dict) and isinstance(data.get("credits"), dict)): return data - credits, costs = data.get("credits"), data.get("costs") - if not isinstance(credits, dict): + if any(k in data for k in ("verify", "ask", "assess", "quota_resets_at")): return data - out = dict(data) - if "quota_resets_at" not in out: - out["quota_resets_at"] = credits.get("resets_at") - if not isinstance(costs, dict): - return out + credits, costs = data["credits"], data.get("costs") def _int(value: Any) -> int: return value if isinstance(value, int) and not isinstance(value, bool) else 0 - total, remaining, extra = ( - _int(credits.get("total")), - _int(credits.get("remaining")), - _int(credits.get("extra", credits.get("bonus"))), - ) + out = _fill(data, quota_resets_at=credits.get("resets_at")) + if not isinstance(costs, dict): + return out + total, remaining = _int(credits.get("total")), _int(credits.get("remaining")) + extra = _int(credits.get("extra", credits.get("bonus"))) for capability in ("verify", "ask", "assess"): cost = _int(costs.get(capability)) - if capability in out or cost <= 0: - continue - quota_total, left = total // cost, remaining // cost - out[capability] = { - "quota_used": quota_total - left, - "quota_total": quota_total, - "quota_remaining": left, - "bonus": extra // cost, - "remaining": left, - } + if cost > 0: + quota_total, left = total // cost, remaining // cost + out[capability] = { + "quota_used": quota_total - left, + "quota_total": quota_total, + "quota_remaining": left, + "bonus": extra // cost, + "remaining": left, + } return out @@ -1470,6 +1325,45 @@ class ReviewStarted(_Lax): status: str = "queued" +class FailureBlock(_Lax): + """Why a review, or one claim's work inside it, failed. + + The same fields a failed ``GET /verify/status`` carries: ``failure_reason`` + is the specific cause (an open set, e.g. ``no_claim``, + ``insufficient_credits``, ``timeout``), ``failure_class`` the closed + :data:`FailureClass`, ``retryable`` whether resending the same request can + help, ``hint`` one sentence on what to do next and ``docs_url`` where the + class is explained. + """ + + failure_reason: str | None = "" + failure_class: str | None = "" + retryable: bool | None = None + hint: str | None = None + docs_url: str | None = "" + + @model_validator(mode="before") + @classmethod + def _read_newer_shape(cls, data: Any) -> Any: + if _is_newer(data, "code", "failure_reason"): + return _fill(data, failure_reason=_old_code(data["code"], "no_claim")) + return data + + @property + def code(self) -> str | None: + """The cause, an open set (``no_checkable_claim``, ``timeout``, ...). + ``failure_reason`` is its older name, spelling "nothing checkable" + ``no_claim``.""" + sent = _sent(self, "code") + return sent if isinstance(sent, str) else _new_code(self.failure_reason) + + @property + def detail(self) -> str | None: + """One sentence on what happened; ``None`` from the original shape.""" + sent = _sent(self, "detail") + return sent if isinstance(sent, str) else None + + class EscalationPolicy(_Lax): """Which quick-checked claims get a deep check, as the review RESOLVED it. @@ -1537,19 +1431,11 @@ class ReviewSummary(_Lax): The ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review that did not ask for the citation check.""" - #: How many claims the draft holds. ``None`` until the draft is read, - #: and from the original response shape, which does not send it. - claims_found: int | None = None claims_selected: int | None = None #: The resolved ``max_assessments``. claim_limit: int = 20 - #: Some of the draft's claims were left out (more than ``claim_limit``): - #: they are in ``more_claims``. From the original response shape it is - #: read off ``more_claims`` once the draft is read. - claim_limit_exceeded: bool | None = None - #: Deprecated: the draft held at least ``claim_limit`` claims, so more - #: MAY exist. Read :attr:`claim_limit_exceeded`, which says whether they do. - claim_limit_reached: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) + #: The draft held at least ``claim_limit`` claims: more MAY exist. + claim_limit_reached: bool | None = None #: The text was cut at 50,000 characters. input_truncated: bool = False assessments: ReviewAssessmentCounts | None = None @@ -1561,10 +1447,8 @@ class ReviewSummary(_Lax): citations_selected: int | None = None #: The resolved ``max_citations``. citation_limit: int | None = None - #: ``citations_found`` is over ``citation_limit``: some were left out. - citation_limit_exceeded: bool | None = None - #: Deprecated: the same value as :attr:`citation_limit_exceeded`. - citation_limit_reached: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) + #: ``citations_found`` is over ``citation_limit``. + citation_limit_reached: bool | None = None citation_checks: ReviewCitationCheckCounts | None = None #: ``len(citation_issues)``. citation_issues: int = 0 @@ -1575,15 +1459,33 @@ class ReviewSummary(_Lax): @model_validator(mode="before") @classmethod - @_reads_both("claims_found", "claim_limit_exceeded", "citation_limit_exceeded") - def _read_both_shapes(cls, data: Any) -> Any: - if not isinstance(data, dict): + def _read_newer_shape(cls, data: Any) -> Any: + if not _is_newer(data, "claim_limit_exceeded", "claim_limit_reached"): return data - out = _mirror(dict(data), "citation_limit_exceeded", "citation_limit_reached") - if "claim_limit_reached" not in out and "claims_found" in out: - found, limit = out.get("claims_found"), out.get("claim_limit", 20) - out["claim_limit_reached"] = found >= limit if isinstance(found, int) and isinstance(limit, int) else None - return out + found, limit = data.get("claims_found"), data.get("claim_limit", 20) + reached = found >= limit if isinstance(found, int) and isinstance(limit, int) else None + return _fill(data, claim_limit_reached=reached, citation_limit_reached=data.get("citation_limit_exceeded")) + + @property + def claims_found(self) -> int | None: + """How many claims the draft holds; ``None`` until read, and from the + original response shape, which does not send it.""" + sent = _sent(self, "claims_found") + return sent if isinstance(sent, int) else None + + @property + def claim_limit_exceeded(self) -> bool | None: + """Some of the draft's claims were left out (they are in + ``more_claims``). ``None`` from the original response shape, which + says only that the limit was reached: read ``more_claims`` there.""" + sent = _sent(self, "claim_limit_exceeded") + return sent if isinstance(sent, bool) else None + + @property + def citation_limit_exceeded(self) -> bool | None: + """``citations_found`` is over ``citation_limit``: some were left out.""" + sent = _sent(self, "citation_limit_exceeded") + return sent if isinstance(sent, bool) else self.citation_limit_reached class ReviewCredits(_Lax): @@ -1608,15 +1510,8 @@ class ReviewResult(_Lax): class ReviewAssessment(_Lax): """A claim's quick check. ``status``: ``pending`` | ``running`` | ``completed`` | ``failed``. On ``completed`` the fields mean what they - mean on an ``/assess`` row; on ``failed`` see ``failure`` (its ``code`` - an open set, as on ``/assess``). ``more_claims`` lists other claims - found in the passage that were not checked. - - Deprecated, kept with their original meaning: ``error_code`` is - ``failure.code`` on a failed row (``no_claim`` where the code reads - ``no_checkable_claim``), ``identified_claims`` is ``more_claims``, and - ``hint`` is the row's own hint from the original response shape - (``None`` from the newer one; read ``failure.hint``).""" + mean on an ``/assess`` row; on ``failed`` see ``error_code`` (an open + set, as on ``/assess``), ``hint`` and ``failure``.""" status: str = "" verdict: str | None = None @@ -1624,13 +1519,9 @@ class ReviewAssessment(_Lax): rationale: str | None = None dissent: str | None = None verification_url: str | None = None - more_claims: list[str] = Field(default_factory=list) - #: Deprecated: ``failure.code`` on a failed row. - error_code: str | None = Field(default=None, json_schema_extra={"deprecated": True}) - #: Deprecated: :attr:`more_claims`. - identified_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) - #: Deprecated. See the class docstring. - hint: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + error_code: str | None = None + identified_claims: list[str] = Field(default_factory=list) + hint: str | None = None #: With ``suggest_edits=True``: the claim with its wrong part corrected, #: from the quick check's reasoning, when it found the claim "False" or #: "Mostly False" with high confidence. ``None`` otherwise, and from @@ -1641,15 +1532,21 @@ class ReviewAssessment(_Lax): @model_validator(mode="before") @classmethod - @_reads_both("more_claims") - def _read_both_shapes(cls, data: Any) -> Any: - if not isinstance(data, dict): + def _read_newer_shape(cls, data: Any) -> Any: + if not _is_newer(data, "more_claims", "identified_claims"): return data - out = _mirror(dict(data), "more_claims", "identified_claims") - failure = out.get("failure") - if "error_code" not in out and isinstance(failure, dict): - out["error_code"] = _old_code(failure.get("code", failure.get("failure_reason")), "no_claim") - return out + failure = data.get("failure") if isinstance(data.get("failure"), dict) else {} + return _fill( + data, + identified_claims=data.get("more_claims") or [], + error_code=_old_code(failure.get("code"), "no_claim") if failure else None, + ) + + @property + def more_claims(self) -> list[str]: + """Other claims found in the passage that were not checked.""" + sent = _sent(self, "more_claims") + return list(sent) if isinstance(sent, list) else list(self.identified_claims) class ReviewEntity(_Lax): @@ -1691,19 +1588,23 @@ class ReviewVerification(_Lax): suggested_rewrite: str | None = None warnings: list[str] = Field(default_factory=list) created_at: str | None = None - #: See ``Verification.completed_at``. - completed_at: str | None = None - #: Deprecated. See ``Verification.modified_at``. - modified_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) + modified_at: str | None = None verification_url: str | None = None url: str | None = None failure: FailureBlock | None = None @model_validator(mode="before") @classmethod - @_reads_both("completed_at") - def _read_both_shapes(cls, data: Any) -> Any: - return _with_completion_times(data) + def _read_newer_shape(cls, data: Any) -> Any: + return _fill_modified_at(data) + + @property + def completed_at(self) -> str | None: + """When the verification completed. From the original response shape it + is known only through ``modified_at``: set when that is, ``None`` on a + same-day completion.""" + sent = _sent(self, "completed_at") + return sent if isinstance(sent, str) else self.modified_at class SuggestedEdit(_Lax): @@ -2005,18 +1906,22 @@ class CitecheckSummary(_Lax): citations_found: int | None = None citations_selected: int | None = None citation_limit: int | None = None - #: ``citations_found`` is over ``citation_limit``: some were left out. - citation_limit_exceeded: bool | None = None - #: Deprecated: the same value as :attr:`citation_limit_exceeded`. - citation_limit_reached: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) + citation_limit_reached: bool | None = None citation_checks: ReviewCitationCheckCounts | None = None citation_issues: int = 0 @model_validator(mode="before") @classmethod - @_reads_both("citation_limit_exceeded") - def _read_both_shapes(cls, data: Any) -> Any: - return _mirror(data, "citation_limit_exceeded", "citation_limit_reached") if isinstance(data, dict) else data + def _read_newer_shape(cls, data: Any) -> Any: + if _is_newer(data, "citation_limit_exceeded", "citation_limit_reached"): + return _fill(data, citation_limit_reached=data["citation_limit_exceeded"]) + return data + + @property + def citation_limit_exceeded(self) -> bool | None: + """``citations_found`` is over ``citation_limit``: some were left out.""" + sent = _sent(self, "citation_limit_exceeded") + return sent if isinstance(sent, bool) else self.citation_limit_reached class Citecheck(_Lax): @@ -2093,15 +1998,6 @@ class ReviewEnvelope(_Lax): more_citations: list[ReviewMoreCitation] | None = None failure: FailureBlock | None = None - @model_validator(mode="after") - def _claim_limit_exceeded_from_more_claims(self) -> ReviewEnvelope: - """The original shape says only that the limit was REACHED; whether - claims were left out is whether ``more_claims`` lists any.""" - summary = self.summary - if summary.claim_limit_exceeded is None and self.more_claims is not None: - summary.claim_limit_exceeded = bool(self.more_claims) - return self - class ReviewFull(ReviewEnvelope): """``GET /reviews/{review_id}``: the envelope plus every claim.""" diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index a28409b..b1141ee 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -34,15 +34,16 @@ from datetime import datetime, timezone from typing import Any +from pydantic import ValidationError + from .errors import LenzWebhookSignatureError from .models import ( CandidateClaim, Citecheck, FailureBlock, ReviewFull, - _new_code, + _fill_modified_at, _old_code, - _with_completion_times, ) SIGNATURE_HEADER = "X-Lenz-Signature" @@ -95,24 +96,11 @@ def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool: # ── Typed events ───────────────────────────────────────────────────────── -# -# Two payload shapes reach the same events. The original one is flat for -# ``verification.*`` (``result``, ``needs_input``, ``error`` at the top) and -# nested for ``review.*`` / ``citecheck.*``. The newer one is one envelope for -# every event: ``event``, ``event_id``, the work's id, ``status``, and the body -# polling returns under the work's kind (``verification``, ``review``, -# ``citecheck``). Every attribute below is filled from either shape, and the -# original attributes keep their original meaning. @dataclass class WebhookEvent: - """Base class. Use ``isinstance`` to discriminate the union. - - ``event_id`` is the same on every delivery attempt of one event, while - ``attempt`` counts up: deduplicate on it. ``""`` when a payload does not - carry it (the original ``verification.*`` and ``certificate.*`` shape). - """ + """Base class. Use ``isinstance`` to discriminate the union.""" event: str task_id: str @@ -122,18 +110,19 @@ class WebhookEvent: batch_id: str | None = None status: str = "" raw: dict[str, Any] = field(default_factory=dict, repr=False) - # Keyword-only, so positional construction of every event keeps its order. - event_id: str = field(default="", kw_only=True) + + @property + def event_id(self) -> str: + """The same on every delivery attempt of one event: deduplicate on it. + ``""`` when the payload carries none (the original ``verification.*`` + and ``certificate.*`` payloads).""" + value = self.raw.get("event_id") + return value if isinstance(value, str) else "" @dataclass class VerificationCompleted(WebhookEvent): - """``event=verification.completed`` — the pipeline produced a verdict. - - ``result`` is the verification as a dict, the same body - ``verifications.get`` returns, as sent; from the newer payload shape, - which sends ``completed_at``, the deprecated ``modified_at`` is added. - """ + """``event=verification.completed`` — the pipeline produced a verdict.""" result: dict[str, Any] = field(default_factory=dict) @@ -142,22 +131,33 @@ class VerificationCompleted(WebhookEvent): class VerificationFailed(WebhookEvent): """``event=verification.failed`` — the pipeline terminated without a verdict. - ``failure`` says why: ``failure.code`` is the cause (an open set), - ``failure.detail`` one sentence on what happened, ``failure.failure_class`` - the closed set (``upstream_unavailable`` | ``insufficient_evidence`` | - ``invalid_input`` | ``cancelled`` | ``internal``) and ``failure.retryable`` - the derived signal (true iff ``upstream_unavailable``). - - The original attributes keep their meaning: ``error`` is the cause code - (``not_a_claim`` where ``failure.code`` reads ``no_checkable_claim``), - and ``failure_class`` / ``retryable`` repeat the block's. They default when - an older server omits them. + ``failure_class`` is WHY (closed set — ``upstream_unavailable`` | + ``insufficient_evidence`` | ``invalid_input`` | ``cancelled`` | + ``internal``); ``retryable`` is the derived signal (true iff + ``upstream_unavailable``). Both default when an older server omits them. """ error: str = "" failure_class: str = "" retryable: bool | None = None - failure: FailureBlock | None = None + + @property + def failure(self) -> FailureBlock | None: + """Why the run failed, from either payload shape: ``code`` (the cause, + ``no_checkable_claim`` where ``error`` reads ``not_a_claim``), + ``detail`` (one sentence, newer payloads only), ``failure_class``, + ``retryable``, ``hint`` and ``docs_url``. ``None`` when the payload + names no cause.""" + body = self.raw.get("verification") + block = body.get("failure") if isinstance(body, dict) else None + if not isinstance(block, dict): + if not (self.error or self.failure_class): + return None + block = {"failure_reason": self.error, "failure_class": self.failure_class, "retryable": self.retryable} + try: + return FailureBlock.model_validate(block) + except ValidationError: + return None @dataclass @@ -168,18 +168,34 @@ class VerificationNeedsInput(WebhookEvent): pipeline run produces a ``verification.completed`` (or another ``needs_input``) event. - ``reason`` is why (``multi_claim``), ``claims`` the options to pick from - (each with ``claim`` and ``domain``) and ``hint`` one sentence on what was - unclear and how to resolve it (``""`` when an older server omits it). - ``needs_input`` is the original dict, ``{"reason", "claims", "hint"}``, - whose options carry the claim as ``text``: as sent, or built from the - newer payload shape. + ``hint`` is one sentence on what was unclear and how to resolve it + (``needs_input["hint"]`` on the wire); ``""`` when an older server omits + it. """ needs_input: dict[str, Any] = field(default_factory=dict) hint: str = "" - reason: str = "" - claims: list[CandidateClaim] = field(default_factory=list) + + @property + def reason(self) -> str: + """Why the run paused (``multi_claim``).""" + value = self.needs_input.get("reason") if isinstance(self.needs_input, dict) else None + return value if isinstance(value, str) else "" + + @property + def claims(self) -> list[CandidateClaim]: + """The options to pick from, each with ``claim`` and ``domain``. An + option that cannot be read is left out, never raised on.""" + options = self.needs_input.get("claims") if isinstance(self.needs_input, dict) else None + out = [] + for option in options or []: + if not isinstance(option, dict): + continue + try: + out.append(CandidateClaim.model_validate({k: v for k, v in option.items() if v is not None})) + except ValidationError: + continue + return out @dataclass @@ -211,10 +227,10 @@ class ReviewEvent(WebhookEvent): ``review.issues``. Deduplicate on ``event_id``: it is the same on every delivery attempt of - one event, while ``attempt`` counts up. ``task_id`` (deprecated) - identifies the delivery and cannot be polled on ``/verify/status``; it is - ``""`` on a payload that does not carry it (the newer shape). The deep - checks a review runs send no ``verification.*`` events of their own. + one event, while ``attempt`` counts up. ``task_id`` identifies the + delivery and cannot be polled on ``/verify/status``; the newer payload + shape leaves it out (it reads ``""``). The deep checks a + review runs send no ``verification.*`` events of their own. """ event_id: str = "" @@ -230,9 +246,9 @@ class CitecheckEvent(WebhookEvent): ``citecheck`` is the final check (the same body ``get_citecheck`` returns), or ``None`` if it could not be parsed (``raw["citecheck"]`` still has it). Deduplicate on ``event_id``: it is the same on every - delivery attempt of one event. ``task_id`` (deprecated) identifies the - delivery and cannot be polled on ``/verify/status``; it is ``""`` on a - payload that does not carry it (the newer shape). + delivery attempt of one event. ``task_id`` identifies the delivery and + cannot be polled on ``/verify/status``; the newer payload shape leaves it + out (it reads ``""``). """ event_id: str = "" @@ -240,125 +256,152 @@ class CitecheckEvent(WebhookEvent): citecheck: Citecheck | None = None -def _dict(value: Any) -> dict[str, Any]: - return value if isinstance(value, dict) else {} - - -def _opt_id(*values: Any) -> str | None: - for value in values: - if value: - return str(value) - return None - - -def _legacy_needs_input(payload: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]: - """The original ``needs_input`` dict: as sent, or built from the newer - envelope's polled body (each option carrying the claim as ``text``).""" - if isinstance(payload.get("needs_input"), dict): - return payload["needs_input"] # type: ignore[no-any-return] - claims = [] - for option in body.get("claims") or []: - if isinstance(option, dict): - claims.append({"text": option.get("claim", option.get("text")), "domain": option.get("domain", "")}) - out: dict[str, Any] = {"reason": body.get("reason", "")} - out["claims"] = claims - out["hint"] = body.get("hint", "") - return out +def _original_view(payload: dict[str, Any]) -> dict[str, Any]: + """The original flat ``verification.*`` fields, for a payload in the newer + envelope (the polled body nested under ``verification``). Any other + payload is returned as it is.""" + body = payload.get("verification") + if not isinstance(body, dict): + return payload + view = dict(payload) + view.setdefault("task_id", body.get("task_id")) + result = body.get("result") + if isinstance(result, dict): + view.setdefault("result", _fill_modified_at(result)) + view.setdefault("verification_id", result.get("verification_id")) + failure = body.get("failure") + if isinstance(failure, dict): + view.setdefault("error", _old_code(failure.get("code"), "not_a_claim")) + view.setdefault("failure_class", failure.get("failure_class")) + view.setdefault("retryable", failure.get("retryable")) + if body.get("status") == "needs_input": + options = [ + {"text": o.get("claim", o.get("text")), "domain": o.get("domain", "")} + for o in body.get("claims") or [] + if isinstance(o, dict) + ] + view.setdefault( + "needs_input", {"reason": body.get("reason", ""), "claims": options, "hint": body.get("hint", "")} + ) + view.setdefault("coverage", body.get("coverage") or (result or {}).get("coverage")) + return view -def _build_event(payload: dict[str, Any]) -> WebhookEvent: +def _build_event(raw: dict[str, Any]) -> WebhookEvent: """Discriminate on ``event`` and return the right typed dataclass.""" + payload = _original_view(raw) event = str(payload.get("event") or "") - # The newer envelope nests the polled body under the work's kind. - body = _dict(payload.get("verification")) - result = _dict(payload.get("result")) or _dict(body.get("result")) - event_id = str(payload.get("event_id") or "") - task_id = str(payload.get("task_id") or body.get("task_id") or "") + task_id = str(payload.get("task_id") or "") try: attempt = int(payload.get("attempt") or 1) except (TypeError, ValueError): attempt = 1 delivered_at = str(payload.get("delivered_at") or "") - verification_id = _opt_id(payload.get("verification_id"), result.get("verification_id")) - batch_id = _opt_id(payload.get("batch_id"), body.get("batch_id")) - status = str(payload.get("status") or body.get("status") or "") - common: dict[str, Any] = { - "event": event, - "task_id": task_id, - "attempt": attempt, - "delivered_at": delivered_at, - "verification_id": verification_id, - "batch_id": batch_id, - "status": status, - "raw": payload, - "event_id": event_id, - } + verification_id = str(payload["verification_id"]) if payload.get("verification_id") else None + batch_id = str(payload["batch_id"]) if payload.get("batch_id") else None + status = str(payload.get("status") or "") if event == "verification.completed": - if result and "modified_at" not in result: - # The newer shape: add the original ``modified_at`` (computed), - # leaving an original-shape result exactly as sent. - result = _with_completion_times(result) - return VerificationCompleted(**common, result=result) + return VerificationCompleted( + event=event, + task_id=task_id, + attempt=attempt, + delivered_at=delivered_at, + verification_id=verification_id, + batch_id=batch_id, + status=status, + raw=raw, + result=payload.get("result") or {}, + ) if event == "verification.failed": - block = _dict(body.get("failure")) or _dict(payload.get("failure")) - failure = FailureBlock.model_validate(block) if block else None - legacy_error = payload.get("error") - error = legacy_error if isinstance(legacy_error, str) and legacy_error else "" - if not error and failure is not None: - error = str(_old_code(failure.code, "not_a_claim") or "") - failure_class = payload.get("failure_class") or (failure.failure_class if failure else "") or "" - retryable = payload.get("retryable") - if not isinstance(retryable, bool): - retryable = failure.retryable if failure is not None else None - if failure is None and (error or failure_class): - # The original shape: the block from the flat fields. - failure = FailureBlock.model_validate( - {"code": _new_code(error), "failure_class": failure_class, "retryable": retryable} - ) return VerificationFailed( - **common, - error=error, - failure_class=str(failure_class), - retryable=retryable if isinstance(retryable, bool) else None, - failure=failure, + event=event, + task_id=task_id, + attempt=attempt, + delivered_at=delivered_at, + verification_id=verification_id, + batch_id=batch_id, + status=status, + raw=raw, + error=str(payload.get("error") or ""), + failure_class=str(payload.get("failure_class") or ""), + retryable=payload.get("retryable") if isinstance(payload.get("retryable"), bool) else None, ) if event == "verification.needs_input": - needs_input = _legacy_needs_input(payload, body) + needs_input = payload.get("needs_input") or {} return VerificationNeedsInput( - **common, + event=event, + task_id=task_id, + attempt=attempt, + delivered_at=delivered_at, + verification_id=verification_id, + batch_id=batch_id, + status=status, + raw=raw, needs_input=needs_input, - hint=str(needs_input.get("hint") or ""), - reason=str(needs_input.get("reason") or ""), - claims=[CandidateClaim.model_validate(c) for c in needs_input.get("claims") or [] if isinstance(c, dict)], + hint=str(needs_input.get("hint") or "") if isinstance(needs_input, dict) else "", ) if event == "certificate.timestamped": - coverage = _dict(payload.get("coverage")) or _dict(body.get("coverage")) or _dict(result.get("coverage")) - return CertificateTimestamped(**common, coverage=coverage) + return CertificateTimestamped( + event=event, + task_id=task_id, + attempt=attempt, + delivered_at=delivered_at, + verification_id=verification_id, + batch_id=batch_id, + status=status, + raw=raw, + coverage=payload.get("coverage") or {}, + ) if event in ("review.completed", "review.failed"): - review_body = payload.get("review") + body = payload.get("review") try: - review = ReviewFull.model_validate(review_body) if isinstance(review_body, dict) else None + review = ReviewFull.model_validate(body) if isinstance(body, dict) else None except ValueError: review = None return ReviewEvent( - **common, - review_id=str(payload.get("review_id") or _dict(review_body).get("review_id") or ""), + event=event, + task_id=task_id, + attempt=attempt, + delivered_at=delivered_at, + verification_id=verification_id, + batch_id=batch_id, + status=status, + raw=raw, + event_id=str(payload.get("event_id") or ""), + review_id=str(payload.get("review_id") or ""), review=review, ) if event in ("citecheck.completed", "citecheck.failed"): - check_body = payload.get("citecheck") + body = payload.get("citecheck") try: - check = Citecheck.model_validate(check_body) if isinstance(check_body, dict) else None + check = Citecheck.model_validate(body) if isinstance(body, dict) else None except ValueError: check = None return CitecheckEvent( - **common, - citecheck_id=str(payload.get("citecheck_id") or _dict(check_body).get("citecheck_id") or ""), + event=event, + task_id=task_id, + attempt=attempt, + delivered_at=delivered_at, + verification_id=verification_id, + batch_id=batch_id, + status=status, + raw=raw, + event_id=str(payload.get("event_id") or ""), + citecheck_id=str(payload.get("citecheck_id") or ""), citecheck=check, ) # Unknown event type — return generic. Future-compatible: ignore events # you do not handle rather than failing the delivery. - return WebhookEvent(**common) + return WebhookEvent( + event=event, + task_id=task_id, + attempt=attempt, + delivered_at=delivered_at, + verification_id=verification_id, + batch_id=batch_id, + status=status, + raw=raw, + ) def parse_webhook(body: bytes | str | dict[str, Any]) -> WebhookEvent: diff --git a/tests/fixtures/parity/canonical/account__api_root.json b/tests/fixtures/parity/canonical/account__api_root.json new file mode 100644 index 0000000..592cd14 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__api_root.json @@ -0,0 +1,43 @@ +{ + "status": 200, + "body": { + "name": "Lenz Public API", + "version": "v1", + "docs_url": "https://lenz.io/api/v1/docs/", + "openapi_url": "https://lenz.io/api/v1/openapi.json", + "quickstart_url": "https://lenz.io/docs/quickstart", + "developer_portal_url": "https://lenz.io/developers", + "endpoints": { + "extract": "https://lenz.io/api/v1/extract", + "assess": "https://lenz.io/api/v1/assess", + "verify": "https://lenz.io/api/v1/verify", + "ask": "https://lenz.io/api/v1/ask/{verification_id}", + "verify_status": "https://lenz.io/api/v1/verify/status/{task_id}", + "verifications": "https://lenz.io/api/v1/verifications/{verification_id}", + "library": "https://lenz.io/api/v1/library", + "usage": "https://lenz.io/api/v1/me/usage" + }, + "sdks": { + "python": { + "repository": "https://github.com/lenzhq/lenz-io-python", + "package": "https://pypi.org/project/lenz-io/", + "install": "pip install lenz-io" + }, + "node": { + "repository": "https://github.com/lenzhq/lenz-io-node", + "package": "https://www.npmjs.com/package/lenz-io", + "install": "npm install lenz-io" + } + }, + "mcp": { + "url": "https://lenz.io/mcp", + "transport": "streamable-http", + "repository": "https://github.com/lenzhq/lenz-mcp" + } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__ask_history_after_reset.json b/tests/fixtures/parity/canonical/account__ask_history_after_reset.json new file mode 100644 index 0000000..e9962b2 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__ask_history_after_reset.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "body": { + "messages": [], + "exchanges_used": 0, + "exchange_limit": 100, + "can_send": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__ask_history_after_send.json b/tests/fixtures/parity/canonical/account__ask_history_after_send.json new file mode 100644 index 0000000..ded388d --- /dev/null +++ b/tests/fixtures/parity/canonical/account__ask_history_after_send.json @@ -0,0 +1,25 @@ +{ + "status": 200, + "body": { + "messages": [ + { + "role": "user", + "content": "Why?", + "created_at": "2026-10-01T09:07:00.123456+00:00" + }, + { + "role": "expert", + "content": "Because **evidence**.", + "created_at": "2026-10-01T09:14:00.123456+00:00" + } + ], + "exchanges_used": 1, + "exchange_limit": 100, + "can_send": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__ask_history_empty.json b/tests/fixtures/parity/canonical/account__ask_history_empty.json new file mode 100644 index 0000000..e9962b2 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__ask_history_empty.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "body": { + "messages": [], + "exchanges_used": 0, + "exchange_limit": 100, + "can_send": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__ask_history_messages.json b/tests/fixtures/parity/canonical/account__ask_history_messages.json new file mode 100644 index 0000000..efc4809 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__ask_history_messages.json @@ -0,0 +1,25 @@ +{ + "status": 200, + "body": { + "messages": [ + { + "role": "user", + "content": "Hello", + "created_at": "2026-10-01T09:07:00.123456+00:00" + }, + { + "role": "expert", + "content": "Hi there!", + "created_at": "2026-10-01T09:14:00.123456+00:00" + } + ], + "exchanges_used": 0, + "exchange_limit": 100, + "can_send": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__ask_reset.json b/tests/fixtures/parity/canonical/account__ask_reset.json new file mode 100644 index 0000000..cddadae --- /dev/null +++ b/tests/fixtures/parity/canonical/account__ask_reset.json @@ -0,0 +1,11 @@ +{ + "status": 200, + "body": { + "ok": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__ask_send.json b/tests/fixtures/parity/canonical/account__ask_send.json new file mode 100644 index 0000000..d07ac92 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__ask_send.json @@ -0,0 +1,13 @@ +{ + "status": 200, + "body": { + "role": "expert", + "content": "Because **evidence**.", + "created_at": "2026-10-01T09:07:00.123456+00:00" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__ask_send_language.json b/tests/fixtures/parity/canonical/account__ask_send_language.json new file mode 100644 index 0000000..37c151c --- /dev/null +++ b/tests/fixtures/parity/canonical/account__ask_send_language.json @@ -0,0 +1,13 @@ +{ + "status": 200, + "body": { + "role": "expert", + "content": "Porque.", + "created_at": "2026-10-01T09:07:00.123456+00:00" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__library_empty.json b/tests/fixtures/parity/canonical/account__library_empty.json index b419316..406be8e 100644 --- a/tests/fixtures/parity/canonical/account__library_empty.json +++ b/tests/fixtures/parity/canonical/account__library_empty.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 0, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__library_item_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/account__library_item_modified_at_crosses_midnight_by_minutes.json index 1045857..b9e2003 100644 --- a/tests/fixtures/parity/canonical/account__library_item_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/canonical/account__library_item_modified_at_crosses_midnight_by_minutes.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -26,5 +21,10 @@ "total": 1, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__library_item_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/account__library_item_modified_at_same_day_hours_apart.json index d4d580f..cd1c5f3 100644 --- a/tests/fixtures/parity/canonical/account__library_item_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/canonical/account__library_item_modified_at_same_day_hours_apart.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -26,5 +21,10 @@ "total": 1, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__library_items.json b/tests/fixtures/parity/canonical/account__library_items.json index e810be9..7939f55 100644 --- a/tests/fixtures/parity/canonical/account__library_items.json +++ b/tests/fixtures/parity/canonical/account__library_items.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -18,12 +13,12 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "language": "en" }, { - "verification_id": "7c83247c", + "verification_id": "4d44dfaf", "claim": "Library claim 1.", "domain": "Science", "entities": [], @@ -33,12 +28,12 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:28:00.123456+00:00", - "completed_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", "language": "en" }, { - "verification_id": "b79019e4", + "verification_id": "7c83247c", "claim": "Library claim 2.", "domain": "Science", "entities": [], @@ -48,13 +43,18 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:42:00.123456+00:00", - "completed_at": "2026-10-01T09:42:00.123456+00:00", + "created_at": "2026-10-01T09:21:00.123456+00:00", + "completed_at": "2026-10-01T09:21:00.123456+00:00", "language": "en" } ], "total": 3, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__library_no_match.json b/tests/fixtures/parity/canonical/account__library_no_match.json index b419316..406be8e 100644 --- a/tests/fixtures/parity/canonical/account__library_no_match.json +++ b/tests/fixtures/parity/canonical/account__library_no_match.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 0, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__library_page_past_end.json b/tests/fixtures/parity/canonical/account__library_page_past_end.json index 86d64c7..575d59b 100644 --- a/tests/fixtures/parity/canonical/account__library_page_past_end.json +++ b/tests/fixtures/parity/canonical/account__library_page_past_end.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 1, "page": 2, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__library_paginated_second_page.json b/tests/fixtures/parity/canonical/account__library_paginated_second_page.json index ae61ec7..e19a6d7 100644 --- a/tests/fixtures/parity/canonical/account__library_paginated_second_page.json +++ b/tests/fixtures/parity/canonical/account__library_paginated_second_page.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -18,12 +13,12 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "language": "en" }, { - "verification_id": "7c83247c", + "verification_id": "4d44dfaf", "claim": "Paged claim 021.", "domain": "", "entities": [], @@ -33,13 +28,18 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:28:00.123456+00:00", - "completed_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", "language": "en" } ], "total": 22, "page": 2, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__me_usage_extra_credits.json b/tests/fixtures/parity/canonical/account__me_usage_extra_credits.json index 14ab818..0bd9f0a 100644 --- a/tests/fixtures/parity/canonical/account__me_usage_extra_credits.json +++ b/tests/fixtures/parity/canonical/account__me_usage_extra_credits.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", @@ -13,7 +8,7 @@ "used": 0, "remaining": 350, "extra": 250, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -36,5 +31,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__me_usage_extra_only.json b/tests/fixtures/parity/canonical/account__me_usage_extra_only.json index 3b3e240..553553f 100644 --- a/tests/fixtures/parity/canonical/account__me_usage_extra_only.json +++ b/tests/fixtures/parity/canonical/account__me_usage_extra_only.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", @@ -13,7 +8,7 @@ "used": 100, "remaining": 37, "extra": 37, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -36,5 +31,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__me_usage_extract_calls.json b/tests/fixtures/parity/canonical/account__me_usage_extract_calls.json index 54303ab..0f0a7e3 100644 --- a/tests/fixtures/parity/canonical/account__me_usage_extract_calls.json +++ b/tests/fixtures/parity/canonical/account__me_usage_extract_calls.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", @@ -13,7 +8,7 @@ "used": 0, "remaining": 100, "extra": 0, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -36,5 +31,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__me_usage_free.json b/tests/fixtures/parity/canonical/account__me_usage_free.json index e9b9231..49d68a6 100644 --- a/tests/fixtures/parity/canonical/account__me_usage_free.json +++ b/tests/fixtures/parity/canonical/account__me_usage_free.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", @@ -13,7 +8,7 @@ "used": 0, "remaining": 100, "extra": 0, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -36,5 +31,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__me_usage_free_partly_spent.json b/tests/fixtures/parity/canonical/account__me_usage_free_partly_spent.json index d08a9ac..4ef2fed 100644 --- a/tests/fixtures/parity/canonical/account__me_usage_free_partly_spent.json +++ b/tests/fixtures/parity/canonical/account__me_usage_free_partly_spent.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", @@ -13,7 +8,7 @@ "used": 17, "remaining": 83, "extra": 0, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -36,5 +31,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__me_usage_oauth.json b/tests/fixtures/parity/canonical/account__me_usage_oauth.json new file mode 100644 index 0000000..49d68a6 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__me_usage_oauth.json @@ -0,0 +1,40 @@ +{ + "status": 200, + "body": { + "plan": "free", + "plan_label": "Free", + "credits": { + "total": 100, + "used": 0, + "remaining": 100, + "extra": 0, + "resets_at": "2026-10-01T14:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__me_usage_plus.json b/tests/fixtures/parity/canonical/account__me_usage_plus.json index 4a255a9..d1c06bf 100644 --- a/tests/fixtures/parity/canonical/account__me_usage_plus.json +++ b/tests/fixtures/parity/canonical/account__me_usage_plus.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "plus", "plan_label": "Plus", @@ -13,7 +8,7 @@ "used": 0, "remaining": 500, "extra": 0, - "resets_at": "2026-10-01T09:07:00.123456+00:00" + "resets_at": "2026-10-01T14:07:00.123456+00:00" }, "costs": { "verify": 10, @@ -36,5 +31,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__me_usage_pro.json b/tests/fixtures/parity/canonical/account__me_usage_pro.json index 943f796..0a9924e 100644 --- a/tests/fixtures/parity/canonical/account__me_usage_pro.json +++ b/tests/fixtures/parity/canonical/account__me_usage_pro.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "pro", "plan_label": "Pro", @@ -13,7 +8,7 @@ "used": 0, "remaining": 5000, "extra": 0, - "resets_at": "2026-10-01T09:07:00.123456+00:00" + "resets_at": "2026-10-01T14:07:00.123456+00:00" }, "costs": { "verify": 10, @@ -36,5 +31,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__me_usage_pro_extra.json b/tests/fixtures/parity/canonical/account__me_usage_pro_extra.json index 8f8d482..20a91fa 100644 --- a/tests/fixtures/parity/canonical/account__me_usage_pro_extra.json +++ b/tests/fixtures/parity/canonical/account__me_usage_pro_extra.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "pro", "plan_label": "Pro", @@ -13,7 +8,7 @@ "used": 0, "remaining": 6000, "extra": 1000, - "resets_at": "2026-10-01T09:07:00.123456+00:00" + "resets_at": "2026-10-01T14:07:00.123456+00:00" }, "costs": { "verify": 10, @@ -36,5 +31,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__me_usage_stale_quota_row.json b/tests/fixtures/parity/canonical/account__me_usage_stale_quota_row.json index e9b9231..49d68a6 100644 --- a/tests/fixtures/parity/canonical/account__me_usage_stale_quota_row.json +++ b/tests/fixtures/parity/canonical/account__me_usage_stale_quota_row.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", @@ -13,7 +8,7 @@ "used": 0, "remaining": 100, "extra": 0, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -36,5 +31,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/account__oauth_register_success.json b/tests/fixtures/parity/canonical/account__oauth_register_success.json new file mode 100644 index 0000000..15e9752 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__oauth_register_success.json @@ -0,0 +1,25 @@ +{ + "status": 201, + "body": { + "client_id": "lzc_0a1db81dbba84d2d473826d8", + "client_id_issued_at": "", + "client_name": "n8n", + "redirect_uris": [ + "https://acme.app.n8n.cloud/rest/oauth2-credential/callback" + ], + "grant_types": [ + "authorization_code", + "refresh_token" + ], + "response_types": [ + "code" + ], + "token_endpoint_auth_method": "none", + "scope": "assess verify ask ask:read extract history:read history:write usage:read webhooks:manage" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__oauth_revoke.json b/tests/fixtures/parity/canonical/account__oauth_revoke.json new file mode 100644 index 0000000..f0ea1e6 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__oauth_revoke.json @@ -0,0 +1,9 @@ +{ + "status": 200, + "body": {}, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__oauth_revoke_token.json b/tests/fixtures/parity/canonical/account__oauth_revoke_token.json new file mode 100644 index 0000000..f0ea1e6 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__oauth_revoke_token.json @@ -0,0 +1,9 @@ +{ + "status": 200, + "body": {}, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__webhook_secret_api_key.json b/tests/fixtures/parity/canonical/account__webhook_secret_api_key.json new file mode 100644 index 0000000..4fdb761 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__webhook_secret_api_key.json @@ -0,0 +1,12 @@ +{ + "status": 403, + "body": { + "detail": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "code": "oauth_only" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__webhook_secret_insufficient_scope.json b/tests/fixtures/parity/canonical/account__webhook_secret_insufficient_scope.json new file mode 100644 index 0000000..6e2d697 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__webhook_secret_insufficient_scope.json @@ -0,0 +1,14 @@ +{ + "status": 403, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "webhooks:manage" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"webhooks:manage\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/account__webhook_secret_oauth.json b/tests/fixtures/parity/canonical/account__webhook_secret_oauth.json new file mode 100644 index 0000000..65ec897 --- /dev/null +++ b/tests/fixtures/parity/canonical/account__webhook_secret_oauth.json @@ -0,0 +1,11 @@ +{ + "status": 200, + "body": { + "webhook_secret": "dc06a4fb5512d77eb96f64eedf3ce77dff58de30e17140fbb229a3e048c3bb93" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/assess__401_no_key.json b/tests/fixtures/parity/canonical/assess__401_no_key.json index 1c3df51..dcdaad0 100644 --- a/tests/fixtures/parity/canonical/assess__401_no_key.json +++ b/tests/fixtures/parity/canonical/assess__401_no_key.json @@ -1,12 +1,12 @@ { "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized", - "code": "not_authenticated" } } diff --git a/tests/fixtures/parity/canonical/assess__402_no_credits.json b/tests/fixtures/parity/canonical/assess__402_no_credits.json index 5e931cc..c64d322 100644 --- a/tests/fixtures/parity/canonical/assess__402_no_credits.json +++ b/tests/fixtures/parity/canonical/assess__402_no_credits.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining /assess units.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__422_blank_item.json b/tests/fixtures/parity/canonical/assess__422_blank_item.json index db514d2..f5dbd88 100644 --- a/tests/fixtures/parity/canonical/assess__422_blank_item.json +++ b/tests/fixtures/parity/canonical/assess__422_blank_item.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claims[1] is blank.", "code": "blank_input", @@ -19,5 +14,10 @@ "type": "blank_input" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__422_blank_text.json b/tests/fixtures/parity/canonical/assess__422_blank_text.json index a5a1062..bfcc575 100644 --- a/tests/fixtures/parity/canonical/assess__422_blank_text.json +++ b/tests/fixtures/parity/canonical/assess__422_blank_text.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claim is required.", "code": "blank_input", @@ -18,5 +13,10 @@ "type": "blank_input" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__422_input_conflict.json b/tests/fixtures/parity/canonical/assess__422_input_conflict.json index a46942a..1746713 100644 --- a/tests/fixtures/parity/canonical/assess__422_input_conflict.json +++ b/tests/fixtures/parity/canonical/assess__422_input_conflict.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Send either `claim` (one text) or `claims` (a list of claims), not both.", "code": "input_conflict", @@ -18,5 +13,10 @@ "type": "input_conflict" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__422_item_too_long.json b/tests/fixtures/parity/canonical/assess__422_item_too_long.json index 137d44e..1408fdd 100644 --- a/tests/fixtures/parity/canonical/assess__422_item_too_long.json +++ b/tests/fixtures/parity/canonical/assess__422_item_too_long.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.", "code": "item_too_long", @@ -19,5 +14,10 @@ "type": "item_too_long" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__422_no_input_field.json b/tests/fixtures/parity/canonical/assess__422_no_input_field.json index 9136471..f5b9868 100644 --- a/tests/fixtures/parity/canonical/assess__422_no_input_field.json +++ b/tests/fixtures/parity/canonical/assess__422_no_input_field.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claim: Field required", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "missing" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__422_too_many_items.json b/tests/fixtures/parity/canonical/assess__422_too_many_items.json index 3468b9e..1c4170f 100644 --- a/tests/fixtures/parity/canonical/assess__422_too_many_items.json +++ b/tests/fixtures/parity/canonical/assess__422_too_many_items.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "At most 20 claims per call; got 21. Split the list.", "code": "too_many_items", @@ -18,5 +13,10 @@ "type": "too_many_items" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__422_wrong_type_claim.json b/tests/fixtures/parity/canonical/assess__422_wrong_type_claim.json index acbd190..78a4682 100644 --- a/tests/fixtures/parity/canonical/assess__422_wrong_type_claim.json +++ b/tests/fixtures/parity/canonical/assess__422_wrong_type_claim.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claim: Input should be a valid string", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "string_type" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__422_wrong_type_claims.json b/tests/fixtures/parity/canonical/assess__422_wrong_type_claims.json index 2b44c06..479b4ca 100644 --- a/tests/fixtures/parity/canonical/assess__422_wrong_type_claims.json +++ b/tests/fixtures/parity/canonical/assess__422_wrong_type_claims.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claims: Input should be a valid list", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "list_type" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__502_framing_failed.json b/tests/fixtures/parity/canonical/assess__502_framing_failed.json index e671a1e..3e9296d 100644 --- a/tests/fixtures/parity/canonical/assess__502_framing_failed.json +++ b/tests/fixtures/parity/canonical/assess__502_framing_failed.json @@ -1,12 +1,12 @@ { "status": 502, + "body": { + "detail": "Framing failed", + "code": "framing_failed" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Framing failed", - "code": "framing_failed" } } diff --git a/tests/fixtures/parity/canonical/assess__503_capacity.json b/tests/fixtures/parity/canonical/assess__503_capacity.json index 343e0e1..b10c32e 100644 --- a/tests/fixtures/parity/canonical/assess__503_capacity.json +++ b/tests/fixtures/parity/canonical/assess__503_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "100", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 100, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "100", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__503_list_all_upstream_unavailable.json b/tests/fixtures/parity/canonical/assess__503_list_all_upstream_unavailable.json index c19b99f..f4be3f0 100644 --- a/tests/fixtures/parity/canonical/assess__503_list_all_upstream_unavailable.json +++ b/tests/fixtures/parity/canonical/assess__503_list_all_upstream_unavailable.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Our model providers are temporarily unavailable. Please retry shortly. Nothing was charged.", "code": "upstream_unavailable", "retry_after": 90, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__503_upstream_unavailable.json b/tests/fixtures/parity/canonical/assess__503_upstream_unavailable.json index 3315806..be8c7a9 100644 --- a/tests/fixtures/parity/canonical/assess__503_upstream_unavailable.json +++ b/tests/fixtures/parity/canonical/assess__503_upstream_unavailable.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Our model providers are temporarily unavailable. Please retry shortly.", "code": "upstream_unavailable", "retry_after": 90, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__deep_tier_hit.json b/tests/fixtures/parity/canonical/assess__deep_tier_hit.json index 0a05b10..e988914 100644 --- a/tests/fixtures/parity/canonical/assess__deep_tier_hit.json +++ b/tests/fixtures/parity/canonical/assess__deep_tier_hit.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -24,5 +19,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__idempotency_body_mismatch.json b/tests/fixtures/parity/canonical/assess__idempotency_body_mismatch.json index e20089e..8cdd4be 100644 --- a/tests/fixtures/parity/canonical/assess__idempotency_body_mismatch.json +++ b/tests/fixtures/parity/canonical/assess__idempotency_body_mismatch.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body.", - "code": "idempotency_body_mismatch" } } diff --git a/tests/fixtures/parity/canonical/assess__idempotency_first.json b/tests/fixtures/parity/canonical/assess__idempotency_first.json index 8c05be0..72c4d4a 100644 --- a/tests/fixtures/parity/canonical/assess__idempotency_first.json +++ b/tests/fixtures/parity/canonical/assess__idempotency_first.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -24,5 +19,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__idempotency_replay.json b/tests/fixtures/parity/canonical/assess__idempotency_replay.json index 8c05be0..72c4d4a 100644 --- a/tests/fixtures/parity/canonical/assess__idempotency_replay.json +++ b/tests/fixtures/parity/canonical/assess__idempotency_replay.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -24,5 +19,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__list_all_error_rows.json b/tests/fixtures/parity/canonical/assess__list_all_error_rows.json index 75ced45..f0fc834 100644 --- a/tests/fixtures/parity/canonical/assess__list_all_error_rows.json +++ b/tests/fixtures/parity/canonical/assess__list_all_error_rows.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "no_checkable_claim", "claims": [ @@ -51,5 +46,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__list_compound_item.json b/tests/fixtures/parity/canonical/assess__list_compound_item.json index 536868b..ed32e13 100644 --- a/tests/fixtures/parity/canonical/assess__list_compound_item.json +++ b/tests/fixtures/parity/canonical/assess__list_compound_item.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -40,5 +35,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__list_mixed_rows.json b/tests/fixtures/parity/canonical/assess__list_mixed_rows.json index 5cec126..7b5a292 100644 --- a/tests/fixtures/parity/canonical/assess__list_mixed_rows.json +++ b/tests/fixtures/parity/canonical/assess__list_mixed_rows.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -84,5 +79,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__memo_hit.json b/tests/fixtures/parity/canonical/assess__memo_hit.json index 873c763..5484c2e 100644 --- a/tests/fixtures/parity/canonical/assess__memo_hit.json +++ b/tests/fixtures/parity/canonical/assess__memo_hit.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -24,5 +19,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__replay_later_200.json b/tests/fixtures/parity/canonical/assess__replay_later_200.json index bac5c15..fedc9c5 100644 --- a/tests/fixtures/parity/canonical/assess__replay_later_200.json +++ b/tests/fixtures/parity/canonical/assess__replay_later_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -24,5 +19,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__single_no_claim.json b/tests/fixtures/parity/canonical/assess__single_no_claim.json index 3a193c0..aea089f 100644 --- a/tests/fixtures/parity/canonical/assess__single_no_claim.json +++ b/tests/fixtures/parity/canonical/assess__single_no_claim.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "no_checkable_claim", "claims": [], @@ -17,5 +12,10 @@ "docs_url": "https://lenz.io/docs/errors#invalid-input" }, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__single_one_claim.json b/tests/fixtures/parity/canonical/assess__single_one_claim.json index d27ac90..2c30b26 100644 --- a/tests/fixtures/parity/canonical/assess__single_one_claim.json +++ b/tests/fixtures/parity/canonical/assess__single_one_claim.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -24,5 +19,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__single_text_over_wave_more_claims.json b/tests/fixtures/parity/canonical/assess__single_text_over_wave_more_claims.json index 1d4fc2e..224f888 100644 --- a/tests/fixtures/parity/canonical/assess__single_text_over_wave_more_claims.json +++ b/tests/fixtures/parity/canonical/assess__single_text_over_wave_more_claims.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -274,5 +269,10 @@ "Claim number 20 is documented.", "Claim number 21 is documented." ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__single_text_several_claims.json b/tests/fixtures/parity/canonical/assess__single_text_several_claims.json index 4313609..e2e6e8c 100644 --- a/tests/fixtures/parity/canonical/assess__single_text_several_claims.json +++ b/tests/fixtures/parity/canonical/assess__single_text_several_claims.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -50,5 +45,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/assess__suggest_rewrite.json b/tests/fixtures/parity/canonical/assess__suggest_rewrite.json index d2aee3e..3df8c25 100644 --- a/tests/fixtures/parity/canonical/assess__suggest_rewrite.json +++ b/tests/fixtures/parity/canonical/assess__suggest_rewrite.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ok", "claims": [ @@ -37,5 +32,10 @@ ], "failure": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__402_no_credits.json b/tests/fixtures/parity/canonical/citecheck__402_no_credits.json index dfb7db4..5d36059 100644 --- a/tests/fixtures/parity/canonical/citecheck__402_no_credits.json +++ b/tests/fixtures/parity/canonical/citecheck__402_no_credits.json @@ -1,18 +1,18 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining credits for citation checks.", "code": "no_credits", "docs_url": "https://lenz.io/docs/errors#quota", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 100, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_empty_body.json b/tests/fixtures/parity/canonical/citecheck__422_empty_body.json index 82cdb99..8e95f6b 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_empty_body.json +++ b/tests/fixtures/parity/canonical/citecheck__422_empty_body.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload: Value error, send exactly one of text and pairs", "code": "validation_error", @@ -21,5 +16,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_invalid_doi.json b/tests/fixtures/parity/canonical/citecheck__422_invalid_doi.json index c3c1d4c..def6efd 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_invalid_doi.json +++ b/tests/fixtures/parity/canonical/citecheck__422_invalid_doi.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "pairs[0].doi: Send the DOI alone, like 10.1038/nature12373.", "code": "invalid_doi", @@ -21,5 +16,10 @@ "type": "invalid_doi" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_max_citations_over_20.json b/tests/fixtures/parity/canonical/citecheck__422_max_citations_over_20.json index 610a9cd..bbc3599 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_max_citations_over_20.json +++ b/tests/fixtures/parity/canonical/citecheck__422_max_citations_over_20.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "max_citations: Input should be less than or equal to 20", "code": "validation_error", @@ -22,5 +17,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_max_citations_with_pairs.json b/tests/fixtures/parity/canonical/citecheck__422_max_citations_with_pairs.json index 746f21f..5fb4268 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_max_citations_with_pairs.json +++ b/tests/fixtures/parity/canonical/citecheck__422_max_citations_with_pairs.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload: Value error, max_citations goes with text: every pair is checked", "code": "validation_error", @@ -21,5 +16,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_pair_bad_url_scheme.json b/tests/fixtures/parity/canonical/citecheck__422_pair_bad_url_scheme.json index a791f53..10bcd15 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_pair_bad_url_scheme.json +++ b/tests/fixtures/parity/canonical/citecheck__422_pair_bad_url_scheme.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "pairs[0].url: Value error, url: an http or https link", "code": "validation_error", @@ -24,5 +19,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_pair_no_source.json b/tests/fixtures/parity/canonical/citecheck__422_pair_no_source.json index f7ac540..038c0b4 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_pair_no_source.json +++ b/tests/fixtures/parity/canonical/citecheck__422_pair_no_source.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "pairs[0]: Value error, send exactly one of url and doi", "code": "validation_error", @@ -23,5 +18,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_pair_url_and_doi.json b/tests/fixtures/parity/canonical/citecheck__422_pair_url_and_doi.json index f7ac540..038c0b4 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_pair_url_and_doi.json +++ b/tests/fixtures/parity/canonical/citecheck__422_pair_url_and_doi.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "pairs[0]: Value error, send exactly one of url and doi", "code": "validation_error", @@ -23,5 +18,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_pairs_empty.json b/tests/fixtures/parity/canonical/citecheck__422_pairs_empty.json index 9b2008e..33dbd42 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_pairs_empty.json +++ b/tests/fixtures/parity/canonical/citecheck__422_pairs_empty.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "pairs: Value error, pairs: 1 to 20 items", "code": "validation_error", @@ -22,5 +17,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_pairs_over_20.json b/tests/fixtures/parity/canonical/citecheck__422_pairs_over_20.json index 9b2008e..33dbd42 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_pairs_over_20.json +++ b/tests/fixtures/parity/canonical/citecheck__422_pairs_over_20.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "pairs: Value error, pairs: 1 to 20 items", "code": "validation_error", @@ -22,5 +17,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_quote_not_in_statement.json b/tests/fixtures/parity/canonical/citecheck__422_quote_not_in_statement.json index e4d2408..543ee02 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_quote_not_in_statement.json +++ b/tests/fixtures/parity/canonical/citecheck__422_quote_not_in_statement.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "pairs[0]: Each quote must be words of its statement.", "code": "quote_not_in_statement", @@ -20,5 +15,10 @@ "type": "quote_not_in_statement" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_text_and_pairs.json b/tests/fixtures/parity/canonical/citecheck__422_text_and_pairs.json index 82cdb99..8e95f6b 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_text_and_pairs.json +++ b/tests/fixtures/parity/canonical/citecheck__422_text_and_pairs.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload: Value error, send exactly one of text and pairs", "code": "validation_error", @@ -21,5 +16,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_unknown_field.json b/tests/fixtures/parity/canonical/citecheck__422_unknown_field.json index 0aed242..0823167 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_unknown_field.json +++ b/tests/fixtures/parity/canonical/citecheck__422_unknown_field.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "pairs[0].extra: Extra inputs are not permitted", "code": "validation_error", @@ -21,5 +16,10 @@ "type": "extra_forbidden" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_url_input.json b/tests/fixtures/parity/canonical/citecheck__422_url_input.json index 816edbe..68e0bdf 100644 --- a/tests/fixtures/parity/canonical/citecheck__422_url_input.json +++ b/tests/fixtures/parity/canonical/citecheck__422_url_input.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Send the page's text, with its links, to check the sources it cites.", "code": "url_input", @@ -18,5 +13,10 @@ "type": "url_input" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__422_webhook_secret_missing.json b/tests/fixtures/parity/canonical/citecheck__422_webhook_secret_missing.json new file mode 100644 index 0000000..3286c6c --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__422_webhook_secret_missing.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "body": { + "detail": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "code": "webhook_secret_missing", + "errors": [ + { + "loc": [ + "body", + "webhook_url" + ], + "msg": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "type": "webhook_secret_missing" + } + ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/citecheck__429_citecheck_in_flight.json b/tests/fixtures/parity/canonical/citecheck__429_citecheck_in_flight.json index fa73820..ec8db9b 100644 --- a/tests/fixtures/parity/canonical/citecheck__429_citecheck_in_flight.json +++ b/tests/fixtures/parity/canonical/citecheck__429_citecheck_in_flight.json @@ -1,14 +1,14 @@ { "status": 429, + "body": { + "detail": "This account already has 3 citation checks running. Wait for one to finish.", + "code": "citecheck_in_flight", + "retry_after": 60 + }, "headers": { "Content-Type": "application/json", "Retry-After": "60", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "This account already has 3 citation checks running. Wait for one to finish.", - "code": "citecheck_in_flight", - "retry_after": 60 } } diff --git a/tests/fixtures/parity/canonical/citecheck__503_capacity.json b/tests/fixtures/parity/canonical/citecheck__503_capacity.json index bcff7db..63b5d1c 100644 --- a/tests/fixtures/parity/canonical/citecheck__503_capacity.json +++ b/tests/fixtures/parity/canonical/citecheck__503_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 90, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__503_citations_unavailable.json b/tests/fixtures/parity/canonical/citecheck__503_citations_unavailable.json index cdeb748..6f3c66e 100644 --- a/tests/fixtures/parity/canonical/citecheck__503_citations_unavailable.json +++ b/tests/fixtures/parity/canonical/citecheck__503_citations_unavailable.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "300", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Citation checking is switched off right now. Retry later; nothing was charged.", "code": "citations_unavailable", "retry_after": 300, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "300", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_404_not_found.json b/tests/fixtures/parity/canonical/citecheck__get_404_not_found.json index e0adfb3..5aa3709 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_404_not_found.json +++ b/tests/fixtures/parity/canonical/citecheck__get_404_not_found.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Citation check not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Citation check not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_404_review_id.json b/tests/fixtures/parity/canonical/citecheck__get_404_review_id.json index e0adfb3..5aa3709 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_404_review_id.json +++ b/tests/fixtures/parity/canonical/citecheck__get_404_review_id.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Citation check not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Citation check not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_410_purged.json b/tests/fixtures/parity/canonical/citecheck__get_410_purged.json index 9fff981..08b9585 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_410_purged.json +++ b/tests/fixtures/parity/canonical/citecheck__get_410_purged.json @@ -1,13 +1,13 @@ { "status": 410, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This check is no longer available: its account removes content after a set period.", "code": "purged", "purged_at": "2026-10-01T09:07:00.123456Z" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_checking.json b/tests/fixtures/parity/canonical/citecheck__get_checking.json index 9ca9e92..9205f4d 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_checking.json +++ b/tests/fixtures/parity/canonical/citecheck__get_checking.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "checking", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "poll_after_seconds": 10, @@ -99,5 +94,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_clean.json b/tests/fixtures/parity/canonical/citecheck__get_completed_clean.json index 11e7ed2..a80bfd4 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_completed_clean.json +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_clean.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -107,5 +102,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_doi_pair.json b/tests/fixtures/parity/canonical/citecheck__get_completed_doi_pair.json index 49e62df..c169837 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_completed_doi_pair.json +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_doi_pair.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -72,5 +67,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_incomplete.json b/tests/fixtures/parity/canonical/citecheck__get_completed_incomplete.json index a50708c..02db48c 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_completed_incomplete.json +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_incomplete.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -125,5 +120,10 @@ ], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_issues_found.json b/tests/fixtures/parity/canonical/citecheck__get_completed_issues_found.json index 1555b9a..6be9e27 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_completed_issues_found.json +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_issues_found.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -137,5 +132,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_partly_supported.json b/tests/fixtures/parity/canonical/citecheck__get_completed_partly_supported.json index f1d95be..dc8d58a 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_completed_partly_supported.json +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_partly_supported.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -107,5 +102,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_completed_unchecked.json b/tests/fixtures/parity/canonical/citecheck__get_completed_unchecked.json index a37c4cb..0eb98a2 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_completed_unchecked.json +++ b/tests/fixtures/parity/canonical/citecheck__get_completed_unchecked.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -107,5 +102,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_failed_no_citations.json b/tests/fixtures/parity/canonical/citecheck__get_failed_no_citations.json index 25625db..29d977a 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_failed_no_citations.json +++ b/tests/fixtures/parity/canonical/citecheck__get_failed_no_citations.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -39,5 +34,10 @@ "docs_url": "https://lenz.io/docs/errors#invalid-input" }, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_failed_upstream_unavailable.json b/tests/fixtures/parity/canonical/citecheck__get_failed_upstream_unavailable.json index a323d29..d5ee990 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_failed_upstream_unavailable.json +++ b/tests/fixtures/parity/canonical/citecheck__get_failed_upstream_unavailable.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -149,5 +144,10 @@ "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" }, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_queued.json b/tests/fixtures/parity/canonical/citecheck__get_queued.json index df7dfee..946e493 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_queued.json +++ b/tests/fixtures/parity/canonical/citecheck__get_queued.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "queued", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "poll_after_seconds": 10, @@ -68,5 +63,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_text_limit_exactly_at_limit.json b/tests/fixtures/parity/canonical/citecheck__get_text_limit_exactly_at_limit.json index 600d1b9..403ac29 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_text_limit_exactly_at_limit.json +++ b/tests/fixtures/parity/canonical/citecheck__get_text_limit_exactly_at_limit.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -115,5 +110,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__get_text_limit_reached.json b/tests/fixtures/parity/canonical/citecheck__get_text_limit_reached.json index a4dbb6b..279af9f 100644 --- a/tests/fixtures/parity/canonical/citecheck__get_text_limit_reached.json +++ b/tests/fixtures/parity/canonical/citecheck__get_text_limit_reached.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -89,5 +84,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__idempotency_body_mismatch_422.json b/tests/fixtures/parity/canonical/citecheck__idempotency_body_mismatch_422.json index e20089e..8cdd4be 100644 --- a/tests/fixtures/parity/canonical/citecheck__idempotency_body_mismatch_422.json +++ b/tests/fixtures/parity/canonical/citecheck__idempotency_body_mismatch_422.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body.", - "code": "idempotency_body_mismatch" } } diff --git a/tests/fixtures/parity/canonical/citecheck__idempotency_conflict_409.json b/tests/fixtures/parity/canonical/citecheck__idempotency_conflict_409.json index 3978a83..c29d230 100644 --- a/tests/fixtures/parity/canonical/citecheck__idempotency_conflict_409.json +++ b/tests/fixtures/parity/canonical/citecheck__idempotency_conflict_409.json @@ -1,13 +1,13 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "A check with this Idempotency-Key is still being created. Retry shortly.", "code": "idempotency_conflict", "citecheck_id": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/citecheck__idempotent_replay_202.json b/tests/fixtures/parity/canonical/citecheck__idempotent_replay_202.json index 864551e..43d3598 100644 --- a/tests/fixtures/parity/canonical/citecheck__idempotent_replay_202.json +++ b/tests/fixtures/parity/canonical/citecheck__idempotent_replay_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/citechecks/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "citecheck_id": "8eb49302", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/citecheck__receipt_202.json b/tests/fixtures/parity/canonical/citecheck__receipt_202.json index 864551e..43d3598 100644 --- a/tests/fixtures/parity/canonical/citecheck__receipt_202.json +++ b/tests/fixtures/parity/canonical/citecheck__receipt_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/citechecks/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "citecheck_id": "8eb49302", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/citecheck__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/canonical/citecheck__receipt_202_empty_webhook_url.json index 864551e..43d3598 100644 --- a/tests/fixtures/parity/canonical/citecheck__receipt_202_empty_webhook_url.json +++ b/tests/fixtures/parity/canonical/citecheck__receipt_202_empty_webhook_url.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/citechecks/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "citecheck_id": "8eb49302", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/errors__ask_bad_language.json b/tests/fixtures/parity/canonical/errors__ask_bad_language.json index e543e6d..cf9c91d 100644 --- a/tests/fixtures/parity/canonical/errors__ask_bad_language.json +++ b/tests/fixtures/parity/canonical/errors__ask_bad_language.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", "code": "unsupported_language", @@ -18,5 +13,10 @@ "type": "unsupported_language" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__ask_empty_message.json b/tests/fixtures/parity/canonical/errors__ask_empty_message.json index 86f7435..1b1c071 100644 --- a/tests/fixtures/parity/canonical/errors__ask_empty_message.json +++ b/tests/fixtures/parity/canonical/errors__ask_empty_message.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Message cannot be empty.", "code": "blank_input", @@ -18,5 +13,10 @@ "type": "blank_input" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__ask_failed.json b/tests/fixtures/parity/canonical/errors__ask_failed.json index df45b1c..a3b433f 100644 --- a/tests/fixtures/parity/canonical/errors__ask_failed.json +++ b/tests/fixtures/parity/canonical/errors__ask_failed.json @@ -1,12 +1,12 @@ { "status": 502, + "body": { + "detail": "The chat model could not answer this question.", + "code": "ask_failed" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "The chat model could not answer this question.", - "code": "ask_failed" } } diff --git a/tests/fixtures/parity/canonical/errors__ask_idempotency_key_reused.json b/tests/fixtures/parity/canonical/errors__ask_idempotency_key_reused.json index e20089e..8cdd4be 100644 --- a/tests/fixtures/parity/canonical/errors__ask_idempotency_key_reused.json +++ b/tests/fixtures/parity/canonical/errors__ask_idempotency_key_reused.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body.", - "code": "idempotency_body_mismatch" } } diff --git a/tests/fixtures/parity/canonical/errors__ask_message_too_long.json b/tests/fixtures/parity/canonical/errors__ask_message_too_long.json index 80b5bab..e982806 100644 --- a/tests/fixtures/parity/canonical/errors__ask_message_too_long.json +++ b/tests/fixtures/parity/canonical/errors__ask_message_too_long.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Message too long (max 500 characters).", "code": "validation_error", @@ -18,5 +13,10 @@ "type": "validation_error" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__ask_not_completed.json b/tests/fixtures/parity/canonical/errors__ask_not_completed.json index 9800c71..ad4b430 100644 --- a/tests/fixtures/parity/canonical/errors__ask_not_completed.json +++ b/tests/fixtures/parity/canonical/errors__ask_not_completed.json @@ -1,12 +1,12 @@ { "status": 400, + "body": { + "detail": "Ask is only available for completed verifications.", + "code": "verification_not_ready" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Ask is only available for completed verifications.", - "code": "verification_not_ready" } } diff --git a/tests/fixtures/parity/canonical/errors__ask_reset_not_found.json b/tests/fixtures/parity/canonical/errors__ask_reset_not_found.json index 3eec577..bccfc47 100644 --- a/tests/fixtures/parity/canonical/errors__ask_reset_not_found.json +++ b/tests/fixtures/parity/canonical/errors__ask_reset_not_found.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Verification not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Verification not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/errors__ask_send_not_found.json b/tests/fixtures/parity/canonical/errors__ask_send_not_found.json index 3eec577..bccfc47 100644 --- a/tests/fixtures/parity/canonical/errors__ask_send_not_found.json +++ b/tests/fixtures/parity/canonical/errors__ask_send_not_found.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Verification not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Verification not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/errors__auth_bad_key.json b/tests/fixtures/parity/canonical/errors__auth_bad_key.json index 1c3df51..dcdaad0 100644 --- a/tests/fixtures/parity/canonical/errors__auth_bad_key.json +++ b/tests/fixtures/parity/canonical/errors__auth_bad_key.json @@ -1,12 +1,12 @@ { "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized", - "code": "not_authenticated" } } diff --git a/tests/fixtures/parity/canonical/errors__auth_bad_oauth_token.json b/tests/fixtures/parity/canonical/errors__auth_bad_oauth_token.json new file mode 100644 index 0000000..cc8ef29 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_bad_oauth_token.json @@ -0,0 +1,13 @@ +{ + "status": 401, + "body": { + "detail": "The access token is invalid, expired or revoked.", + "code": "invalid_token" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"invalid_token\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_basic_scheme.json b/tests/fixtures/parity/canonical/errors__auth_basic_scheme.json index 1c3df51..dcdaad0 100644 --- a/tests/fixtures/parity/canonical/errors__auth_basic_scheme.json +++ b/tests/fixtures/parity/canonical/errors__auth_basic_scheme.json @@ -1,12 +1,12 @@ { "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized", - "code": "not_authenticated" } } diff --git a/tests/fixtures/parity/canonical/errors__auth_expired_oauth_token.json b/tests/fixtures/parity/canonical/errors__auth_expired_oauth_token.json new file mode 100644 index 0000000..cc8ef29 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_expired_oauth_token.json @@ -0,0 +1,13 @@ +{ + "status": 401, + "body": { + "detail": "The access token is invalid, expired or revoked.", + "code": "invalid_token" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"invalid_token\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_insufficient_scope.json b/tests/fixtures/parity/canonical/errors__auth_insufficient_scope.json index 15b8696..4c2fcdd 100644 --- a/tests/fixtures/parity/canonical/errors__auth_insufficient_scope.json +++ b/tests/fixtures/parity/canonical/errors__auth_insufficient_scope.json @@ -1,14 +1,14 @@ { "status": 403, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "verify" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"verify\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "The access token does not allow this call.", - "code": "insufficient_scope", - "scope": "verify" } } diff --git a/tests/fixtures/parity/canonical/errors__auth_missing_bearer.json b/tests/fixtures/parity/canonical/errors__auth_missing_bearer.json index 1c3df51..dcdaad0 100644 --- a/tests/fixtures/parity/canonical/errors__auth_missing_bearer.json +++ b/tests/fixtures/parity/canonical/errors__auth_missing_bearer.json @@ -1,12 +1,12 @@ { "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized", - "code": "not_authenticated" } } diff --git a/tests/fixtures/parity/canonical/errors__auth_missing_bearer_post.json b/tests/fixtures/parity/canonical/errors__auth_missing_bearer_post.json index 1c3df51..dcdaad0 100644 --- a/tests/fixtures/parity/canonical/errors__auth_missing_bearer_post.json +++ b/tests/fixtures/parity/canonical/errors__auth_missing_bearer_post.json @@ -1,12 +1,12 @@ { "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized", - "code": "not_authenticated" } } diff --git a/tests/fixtures/parity/canonical/errors__auth_oauth_issuer_off.json b/tests/fixtures/parity/canonical/errors__auth_oauth_issuer_off.json new file mode 100644 index 0000000..dcdaad0 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_oauth_issuer_off.json @@ -0,0 +1,12 @@ +{ + "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_oauth_only.json b/tests/fixtures/parity/canonical/errors__auth_oauth_only.json new file mode 100644 index 0000000..4fdb761 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__auth_oauth_only.json @@ -0,0 +1,12 @@ +{ + "status": 403, + "body": { + "detail": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "code": "oauth_only" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_history.json b/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_history.json index 3445e87..07faa70 100644 --- a/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_history.json +++ b/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_history.json @@ -1,12 +1,12 @@ { "status": 403, + "body": { + "detail": "This report is private.", + "code": "private_claim" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "This report is private.", - "code": "private_claim" } } diff --git a/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_send.json b/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_send.json index 3445e87..07faa70 100644 --- a/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_send.json +++ b/tests/fixtures/parity/canonical/errors__auth_private_claim_ask_send.json @@ -1,12 +1,12 @@ { "status": 403, + "body": { + "detail": "This report is private.", + "code": "private_claim" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "This report is private.", - "code": "private_claim" } } diff --git a/tests/fixtures/parity/canonical/errors__auth_unrecognised_prefix.json b/tests/fixtures/parity/canonical/errors__auth_unrecognised_prefix.json index 1c3df51..dcdaad0 100644 --- a/tests/fixtures/parity/canonical/errors__auth_unrecognised_prefix.json +++ b/tests/fixtures/parity/canonical/errors__auth_unrecognised_prefix.json @@ -1,12 +1,12 @@ { "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized", - "code": "not_authenticated" } } diff --git a/tests/fixtures/parity/canonical/errors__http_error_library_bad_curated.json b/tests/fixtures/parity/canonical/errors__http_error_library_bad_curated.json index 76cbe7a..30b9871 100644 --- a/tests/fixtures/parity/canonical/errors__http_error_library_bad_curated.json +++ b/tests/fixtures/parity/canonical/errors__http_error_library_bad_curated.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Invalid curated list(s). Allowed: trivia", "code": "validation_error", @@ -18,5 +13,10 @@ "type": "validation_error" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__http_error_library_bad_verdict.json b/tests/fixtures/parity/canonical/errors__http_error_library_bad_verdict.json index 09dbe2e..c77d681 100644 --- a/tests/fixtures/parity/canonical/errors__http_error_library_bad_verdict.json +++ b/tests/fixtures/parity/canonical/errors__http_error_library_bad_verdict.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True", "code": "validation_error", @@ -18,5 +13,10 @@ "type": "validation_error" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__method_not_allowed_get_verify.json b/tests/fixtures/parity/canonical/errors__method_not_allowed_get_verify.json new file mode 100644 index 0000000..7b3ec88 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__method_not_allowed_get_verify.json @@ -0,0 +1,12 @@ +{ + "status": 405, + "body": { + "detail": "Method not allowed.", + "code": "method_not_allowed" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__method_not_allowed_post_library.json b/tests/fixtures/parity/canonical/errors__method_not_allowed_post_library.json new file mode 100644 index 0000000..7b3ec88 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__method_not_allowed_post_library.json @@ -0,0 +1,12 @@ +{ + "status": 405, + "body": { + "detail": "Method not allowed.", + "code": "method_not_allowed" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__not_found_ask.json b/tests/fixtures/parity/canonical/errors__not_found_ask.json index 3eec577..bccfc47 100644 --- a/tests/fixtures/parity/canonical/errors__not_found_ask.json +++ b/tests/fixtures/parity/canonical/errors__not_found_ask.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Verification not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Verification not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/errors__not_found_route.json b/tests/fixtures/parity/canonical/errors__not_found_route.json index f1a8c33..775d847 100644 --- a/tests/fixtures/parity/canonical/errors__not_found_route.json +++ b/tests/fixtures/parity/canonical/errors__not_found_route.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/errors__not_found_verification.json b/tests/fixtures/parity/canonical/errors__not_found_verification.json index f1a8c33..775d847 100644 --- a/tests/fixtures/parity/canonical/errors__not_found_verification.json +++ b/tests/fixtures/parity/canonical/errors__not_found_verification.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/errors__not_found_verify_status.json b/tests/fixtures/parity/canonical/errors__not_found_verify_status.json index ed0bc13..8c0eaee 100644 --- a/tests/fixtures/parity/canonical/errors__not_found_verify_status.json +++ b/tests/fixtures/parity/canonical/errors__not_found_verify_status.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Task not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Task not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/errors__oauth_register_disabled.json b/tests/fixtures/parity/canonical/errors__oauth_register_disabled.json new file mode 100644 index 0000000..a16648d --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__oauth_register_disabled.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "body": { + "": "" + }, + "headers": { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__oauth_register_invalid_client_metadata.json b/tests/fixtures/parity/canonical/errors__oauth_register_invalid_client_metadata.json new file mode 100644 index 0000000..461c91d --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__oauth_register_invalid_client_metadata.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "body": { + "error": "invalid_client_metadata", + "error_description": "token_endpoint_auth_method must be none: Lenz registers public clients that use PKCE." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__oauth_register_invalid_redirect_uri.json b/tests/fixtures/parity/canonical/errors__oauth_register_invalid_redirect_uri.json new file mode 100644 index 0000000..63e62ba --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__oauth_register_invalid_redirect_uri.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "body": { + "error": "invalid_redirect_uri", + "error_description": "Each redirect URI must be https, or http on localhost, 127.0.0.1 or [::1], without a fragment." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__oauth_revoke_invalid_client.json b/tests/fixtures/parity/canonical/errors__oauth_revoke_invalid_client.json new file mode 100644 index 0000000..a7a70ef --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__oauth_revoke_invalid_client.json @@ -0,0 +1,13 @@ +{ + "status": 401, + "body": { + "error": "invalid_client", + "error_description": "Client authentication failed." + }, + "headers": { + "Content-Type": "application/json", + "WWW-Authenticate": "Basic realm=\"lenz\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__oauth_token_invalid_client.json b/tests/fixtures/parity/canonical/errors__oauth_token_invalid_client.json new file mode 100644 index 0000000..a7a70ef --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__oauth_token_invalid_client.json @@ -0,0 +1,13 @@ +{ + "status": 401, + "body": { + "error": "invalid_client", + "error_description": "Client authentication failed." + }, + "headers": { + "Content-Type": "application/json", + "WWW-Authenticate": "Basic realm=\"lenz\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__oauth_token_invalid_grant.json b/tests/fixtures/parity/canonical/errors__oauth_token_invalid_grant.json new file mode 100644 index 0000000..88c6f64 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__oauth_token_invalid_grant.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "body": { + "error": "invalid_grant", + "error_description": "The authorization is invalid or has expired." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__oauth_token_missing_grant_type.json b/tests/fixtures/parity/canonical/errors__oauth_token_missing_grant_type.json new file mode 100644 index 0000000..f989e00 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__oauth_token_missing_grant_type.json @@ -0,0 +1,11 @@ +{ + "status": 400, + "body": { + "error": "unsupported_grant_type" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__oauth_token_refresh_invalid_grant.json b/tests/fixtures/parity/canonical/errors__oauth_token_refresh_invalid_grant.json new file mode 100644 index 0000000..88c6f64 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__oauth_token_refresh_invalid_grant.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "body": { + "error": "invalid_grant", + "error_description": "The authorization is invalid or has expired." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__oauth_token_unsupported_grant_type.json b/tests/fixtures/parity/canonical/errors__oauth_token_unsupported_grant_type.json new file mode 100644 index 0000000..f989e00 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__oauth_token_unsupported_grant_type.json @@ -0,0 +1,11 @@ +{ + "status": 400, + "body": { + "error": "unsupported_grant_type" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/errors__payment_required_ask.json b/tests/fixtures/parity/canonical/errors__payment_required_ask.json index e4bc4cf..72c49cc 100644 --- a/tests/fixtures/parity/canonical/errors__payment_required_ask.json +++ b/tests/fixtures/parity/canonical/errors__payment_required_ask.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining ask credits.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__payment_required_assess.json b/tests/fixtures/parity/canonical/errors__payment_required_assess.json index 5e931cc..c64d322 100644 --- a/tests/fixtures/parity/canonical/errors__payment_required_assess.json +++ b/tests/fixtures/parity/canonical/errors__payment_required_assess.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining /assess units.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__payment_required_verify.json b/tests/fixtures/parity/canonical/errors__payment_required_verify.json index 1f73a67..d93b382 100644 --- a/tests/fixtures/parity/canonical/errors__payment_required_verify.json +++ b/tests/fixtures/parity/canonical/errors__payment_required_verify.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining claim checks.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 10 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__payment_required_verify_batch_short.json b/tests/fixtures/parity/canonical/errors__payment_required_verify_batch_short.json index e80d840..45ee9f8 100644 --- a/tests/fixtures/parity/canonical/errors__payment_required_verify_batch_short.json +++ b/tests/fixtures/parity/canonical/errors__payment_required_verify_batch_short.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Insufficient credits for batch.", "code": "no_credits", @@ -13,8 +8,13 @@ "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "requested": 5, "remaining": 3, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 30, "cost": 50 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__rate_limited_extract.json b/tests/fixtures/parity/canonical/errors__rate_limited_extract.json index e45b708..35f093a 100644 --- a/tests/fixtures/parity/canonical/errors__rate_limited_extract.json +++ b/tests/fixtures/parity/canonical/errors__rate_limited_extract.json @@ -1,11 +1,5 @@ { "status": 429, - "headers": { - "Content-Type": "application/json", - "Retry-After": "900", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", "code": "extract_daily_limit", @@ -13,5 +7,11 @@ "retry_after": 900, "docs_url": "https://lenz.io/docs/errors#rate-limits", "upgrade_url": "https://lenz.io/plans" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "900", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__service_unavailable_ask.json b/tests/fixtures/parity/canonical/errors__service_unavailable_ask.json index 94a09cd..baaa646 100644 --- a/tests/fixtures/parity/canonical/errors__service_unavailable_ask.json +++ b/tests/fixtures/parity/canonical/errors__service_unavailable_ask.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "The chat model is unavailable right now. Retry shortly.", "code": "upstream_unavailable", "retry_after": 90, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__service_unavailable_capacity.json b/tests/fixtures/parity/canonical/errors__service_unavailable_capacity.json index cc8fad8..036bd5a 100644 --- a/tests/fixtures/parity/canonical/errors__service_unavailable_capacity.json +++ b/tests/fixtures/parity/canonical/errors__service_unavailable_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "60", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 60, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__validation_malformed_json.json b/tests/fixtures/parity/canonical/errors__validation_malformed_json.json index 9e1f37c..23ac8ec 100644 --- a/tests/fixtures/parity/canonical/errors__validation_malformed_json.json +++ b/tests/fixtures/parity/canonical/errors__validation_malformed_json.json @@ -1,12 +1,12 @@ { "status": 400, + "body": { + "detail": "The request body is not valid JSON.", + "code": "malformed_body" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "The request body is not valid JSON.", - "code": "malformed_body" } } diff --git a/tests/fixtures/parity/canonical/errors__validation_missing_field_hint.json b/tests/fixtures/parity/canonical/errors__validation_missing_field_hint.json index 2cf69f1..a4831ce 100644 --- a/tests/fixtures/parity/canonical/errors__validation_missing_field_hint.json +++ b/tests/fixtures/parity/canonical/errors__validation_missing_field_hint.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "text: Field required", "code": "validation_error", @@ -25,5 +20,10 @@ "content", "bogus" ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__validation_unknown_field_only.json b/tests/fixtures/parity/canonical/errors__validation_unknown_field_only.json index 877dab6..381ab3e 100644 --- a/tests/fixtures/parity/canonical/errors__validation_unknown_field_only.json +++ b/tests/fixtures/parity/canonical/errors__validation_unknown_field_only.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claim: Field required", "code": "validation_error", @@ -24,5 +19,10 @@ "unrecognized_fields": [ "statement" ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__validation_unknown_field_wrong_type.json b/tests/fixtures/parity/canonical/errors__validation_unknown_field_wrong_type.json index 1aae12f..751ca61 100644 --- a/tests/fixtures/parity/canonical/errors__validation_unknown_field_wrong_type.json +++ b/tests/fixtures/parity/canonical/errors__validation_unknown_field_wrong_type.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claims: Input should be a valid list", "code": "validation_error", @@ -23,5 +18,10 @@ "bogus_field", "another" ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__validation_wrong_type.json b/tests/fixtures/parity/canonical/errors__validation_wrong_type.json index 843bab6..7f77b76 100644 --- a/tests/fixtures/parity/canonical/errors__validation_wrong_type.json +++ b/tests/fixtures/parity/canonical/errors__validation_wrong_type.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "text: Input should be a valid string", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "string_type" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/errors__web_assess_payment_required.json b/tests/fixtures/parity/canonical/errors__web_assess_payment_required.json new file mode 100644 index 0000000..5e8991b --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__web_assess_payment_required.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "body": { + "detail": "No assessments left this month.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T14:07:00+00:00", + "credits_remaining": 0, + "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "Cookie" + } +} diff --git a/tests/fixtures/parity/canonical/errors__web_assess_signup_required.json b/tests/fixtures/parity/canonical/errors__web_assess_signup_required.json new file mode 100644 index 0000000..7621cf2 --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__web_assess_signup_required.json @@ -0,0 +1,14 @@ +{ + "status": 403, + "body": { + "detail": "Sign up to check this — it’s free.", + "code": "signup_required", + "signup_claim_stashed": false, + "attempt": "e2fffcdbaf5c6cfc1939f3eaec689a87" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "Cookie" + } +} diff --git a/tests/fixtures/parity/canonical/errors__web_review_rate_limited.json b/tests/fixtures/parity/canonical/errors__web_review_rate_limited.json new file mode 100644 index 0000000..0e7609f --- /dev/null +++ b/tests/fixtures/parity/canonical/errors__web_review_rate_limited.json @@ -0,0 +1,17 @@ +{ + "status": 429, + "body": { + "detail": "This account has used its extractions for today. The daily allowance is shared with the Lenz API. It resets at 00:00 UTC.", + "code": "extract_daily_limit", + "limit": 1000, + "reset_in_seconds": 777, + "doc_url": "https://lenz.io/docs/errors#rate-limits", + "upgrade_url": "https://lenz.io/plans" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "777", + "Cache-Control": "no-store", + "Vary": "Cookie" + } +} diff --git a/tests/fixtures/parity/canonical/extract__401_no_key.json b/tests/fixtures/parity/canonical/extract__401_no_key.json index 1c3df51..dcdaad0 100644 --- a/tests/fixtures/parity/canonical/extract__401_no_key.json +++ b/tests/fixtures/parity/canonical/extract__401_no_key.json @@ -1,12 +1,12 @@ { "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized", - "code": "not_authenticated" } } diff --git a/tests/fixtures/parity/canonical/extract__422_blank_text.json b/tests/fixtures/parity/canonical/extract__422_blank_text.json index 4242aac..9111f0b 100644 --- a/tests/fixtures/parity/canonical/extract__422_blank_text.json +++ b/tests/fixtures/parity/canonical/extract__422_blank_text.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Text is required.", "code": "blank_input", @@ -18,5 +13,10 @@ "type": "blank_input" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__422_focus_too_long.json b/tests/fixtures/parity/canonical/extract__422_focus_too_long.json index 6645b81..228a79f 100644 --- a/tests/fixtures/parity/canonical/extract__422_focus_too_long.json +++ b/tests/fixtures/parity/canonical/extract__422_focus_too_long.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "focus must be at most 300 characters of plain text.", "code": "validation_error", @@ -18,5 +13,10 @@ "type": "validation_error" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__422_missing_text.json b/tests/fixtures/parity/canonical/extract__422_missing_text.json index eb7b0fc..9ce0a7d 100644 --- a/tests/fixtures/parity/canonical/extract__422_missing_text.json +++ b/tests/fixtures/parity/canonical/extract__422_missing_text.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "text: Field required", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "missing" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__429_daily_limit.json b/tests/fixtures/parity/canonical/extract__429_daily_limit.json index 23ae05b..7ed31f0 100644 --- a/tests/fixtures/parity/canonical/extract__429_daily_limit.json +++ b/tests/fixtures/parity/canonical/extract__429_daily_limit.json @@ -1,11 +1,5 @@ { "status": 429, - "headers": { - "Content-Type": "application/json", - "Retry-After": "3600", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", "code": "extract_daily_limit", @@ -13,5 +7,11 @@ "retry_after": 3600, "docs_url": "https://lenz.io/docs/errors#rate-limits", "upgrade_url": "https://lenz.io/plans" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "3600", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__502_extraction_failed.json b/tests/fixtures/parity/canonical/extract__502_extraction_failed.json index 62f2e63..645e6f6 100644 --- a/tests/fixtures/parity/canonical/extract__502_extraction_failed.json +++ b/tests/fixtures/parity/canonical/extract__502_extraction_failed.json @@ -1,12 +1,12 @@ { "status": 502, + "body": { + "detail": "Extraction failed", + "code": "extraction_failed" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Extraction failed", - "code": "extraction_failed" } } diff --git a/tests/fixtures/parity/canonical/extract__503_upstream_unavailable.json b/tests/fixtures/parity/canonical/extract__503_upstream_unavailable.json index 3315806..be8c7a9 100644 --- a/tests/fixtures/parity/canonical/extract__503_upstream_unavailable.json +++ b/tests/fixtures/parity/canonical/extract__503_upstream_unavailable.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Our model providers are temporarily unavailable. Please retry shortly.", "code": "upstream_unavailable", "retry_after": 90, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__focus_ready.json b/tests/fixtures/parity/canonical/extract__focus_ready.json index 29eccf2..6229973 100644 --- a/tests/fixtures/parity/canonical/extract__focus_ready.json +++ b/tests/fixtures/parity/canonical/extract__focus_ready.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -26,5 +21,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__idempotency_body_mismatch_422.json b/tests/fixtures/parity/canonical/extract__idempotency_body_mismatch_422.json index e20089e..8cdd4be 100644 --- a/tests/fixtures/parity/canonical/extract__idempotency_body_mismatch_422.json +++ b/tests/fixtures/parity/canonical/extract__idempotency_body_mismatch_422.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body.", - "code": "idempotency_body_mismatch" } } diff --git a/tests/fixtures/parity/canonical/extract__idempotency_replay.json b/tests/fixtures/parity/canonical/extract__idempotency_replay.json index b508889..d1b6b07 100644 --- a/tests/fixtures/parity/canonical/extract__idempotency_replay.json +++ b/tests/fixtures/parity/canonical/extract__idempotency_replay.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -30,5 +25,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__locate_all_dropped.json b/tests/fixtures/parity/canonical/extract__locate_all_dropped.json index 7906518..2886061 100644 --- a/tests/fixtures/parity/canonical/extract__locate_all_dropped.json +++ b/tests/fixtures/parity/canonical/extract__locate_all_dropped.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "no_checkable_claim", "claims": [], @@ -21,5 +16,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__locate_failed.json b/tests/fixtures/parity/canonical/extract__locate_failed.json index 2fbe6a7..a4ce206 100644 --- a/tests/fixtures/parity/canonical/extract__locate_failed.json +++ b/tests/fixtures/parity/canonical/extract__locate_failed.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -30,5 +25,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__locate_leading_whitespace.json b/tests/fixtures/parity/canonical/extract__locate_leading_whitespace.json index 8033e2d..a84de40 100644 --- a/tests/fixtures/parity/canonical/extract__locate_leading_whitespace.json +++ b/tests/fixtures/parity/canonical/extract__locate_leading_whitespace.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -32,5 +27,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__locate_some_dropped.json b/tests/fixtures/parity/canonical/extract__locate_some_dropped.json index 11529fa..15e296a 100644 --- a/tests/fixtures/parity/canonical/extract__locate_some_dropped.json +++ b/tests/fixtures/parity/canonical/extract__locate_some_dropped.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -42,5 +37,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__locate_true.json b/tests/fixtures/parity/canonical/extract__locate_true.json index 11529fa..15e296a 100644 --- a/tests/fixtures/parity/canonical/extract__locate_true.json +++ b/tests/fixtures/parity/canonical/extract__locate_true.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -42,5 +37,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__no_match_with_focus.json b/tests/fixtures/parity/canonical/extract__no_match_with_focus.json index eceaf0b..b3a7571 100644 --- a/tests/fixtures/parity/canonical/extract__no_match_with_focus.json +++ b/tests/fixtures/parity/canonical/extract__no_match_with_focus.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "no_match", "claims": [], @@ -12,5 +7,10 @@ "key_entities": [], "presumed_intent": "", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__not_a_claim.json b/tests/fixtures/parity/canonical/extract__not_a_claim.json index df06a66..25e526b 100644 --- a/tests/fixtures/parity/canonical/extract__not_a_claim.json +++ b/tests/fixtures/parity/canonical/extract__not_a_claim.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "no_checkable_claim", "claims": [], @@ -12,5 +7,10 @@ "key_entities": [], "presumed_intent": "", "original_input": "What a lovely day." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__not_a_claim_beside_claims.json b/tests/fixtures/parity/canonical/extract__not_a_claim_beside_claims.json index 646bbad..a0569d2 100644 --- a/tests/fixtures/parity/canonical/extract__not_a_claim_beside_claims.json +++ b/tests/fixtures/parity/canonical/extract__not_a_claim_beside_claims.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -26,5 +21,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__ready_one_claim.json b/tests/fixtures/parity/canonical/extract__ready_one_claim.json index 646bbad..a0569d2 100644 --- a/tests/fixtures/parity/canonical/extract__ready_one_claim.json +++ b/tests/fixtures/parity/canonical/extract__ready_one_claim.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -26,5 +21,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__ready_several_claims.json b/tests/fixtures/parity/canonical/extract__ready_several_claims.json index 2fbe6a7..a4ce206 100644 --- a/tests/fixtures/parity/canonical/extract__ready_several_claims.json +++ b/tests/fixtures/parity/canonical/extract__ready_several_claims.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -30,5 +25,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__replay_later_200.json b/tests/fixtures/parity/canonical/extract__replay_later_200.json index 8fff1c2..8f23ae8 100644 --- a/tests/fixtures/parity/canonical/extract__replay_later_200.json +++ b/tests/fixtures/parity/canonical/extract__replay_later_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -27,5 +22,10 @@ "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", "locations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__url_fetch_502_unreadable.json b/tests/fixtures/parity/canonical/extract__url_fetch_502_unreadable.json index 62f2e63..645e6f6 100644 --- a/tests/fixtures/parity/canonical/extract__url_fetch_502_unreadable.json +++ b/tests/fixtures/parity/canonical/extract__url_fetch_502_unreadable.json @@ -1,12 +1,12 @@ { "status": 502, + "body": { + "detail": "Extraction failed", + "code": "extraction_failed" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Extraction failed", - "code": "extraction_failed" } } diff --git a/tests/fixtures/parity/canonical/extract__url_fetch_503_upstream.json b/tests/fixtures/parity/canonical/extract__url_fetch_503_upstream.json index 14eded6..182b135 100644 --- a/tests/fixtures/parity/canonical/extract__url_fetch_503_upstream.json +++ b/tests/fixtures/parity/canonical/extract__url_fetch_503_upstream.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "The page could not be read right now. Please retry shortly.", "code": "upstream_unavailable", "retry_after": 90, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/extract__url_input_located.json b/tests/fixtures/parity/canonical/extract__url_input_located.json index 5c97376..dbed678 100644 --- a/tests/fixtures/parity/canonical/extract__url_input_located.json +++ b/tests/fixtures/parity/canonical/extract__url_input_located.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claims": [ @@ -32,5 +27,10 @@ ], "presumed_intent": "Verify reported figures", "original_input": "https://example.com/an-article" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__401_no_credentials.json b/tests/fixtures/parity/canonical/review__401_no_credentials.json index 1c3df51..dcdaad0 100644 --- a/tests/fixtures/parity/canonical/review__401_no_credentials.json +++ b/tests/fixtures/parity/canonical/review__401_no_credentials.json @@ -1,12 +1,12 @@ { "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized", - "code": "not_authenticated" } } diff --git a/tests/fixtures/parity/canonical/review__402_no_credits.json b/tests/fixtures/parity/canonical/review__402_no_credits.json index 5a9f5be..7185c43 100644 --- a/tests/fixtures/parity/canonical/review__402_no_credits.json +++ b/tests/fixtures/parity/canonical/review__402_no_credits.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining credits to assess the draft.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 100, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 100, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__402_no_credits_exhausted.json b/tests/fixtures/parity/canonical/review__402_no_credits_exhausted.json index 2ec177c..543c798 100644 --- a/tests/fixtures/parity/canonical/review__402_no_credits_exhausted.json +++ b/tests/fixtures/parity/canonical/review__402_no_credits_exhausted.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining credits to assess the draft.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_blank_text.json b/tests/fixtures/parity/canonical/review__422_blank_text.json index ee12b17..784e798 100644 --- a/tests/fixtures/parity/canonical/review__422_blank_text.json +++ b/tests/fixtures/parity/canonical/review__422_blank_text.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "text: send the draft, or one public http(s) URL.", "code": "blank_input", @@ -18,5 +13,10 @@ "type": "blank_input" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_citations_options_object.json b/tests/fixtures/parity/canonical/review__422_citations_options_object.json index 8932e84..010e256 100644 --- a/tests/fixtures/parity/canonical/review__422_citations_options_object.json +++ b/tests/fixtures/parity/canonical/review__422_citations_options_object.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "citations: Extra inputs are not permitted", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "extra_forbidden" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_depth_unknown.json b/tests/fixtures/parity/canonical/review__422_depth_unknown.json index 3ef5a12..1bb9ce0 100644 --- a/tests/fixtures/parity/canonical/review__422_depth_unknown.json +++ b/tests/fixtures/parity/canonical/review__422_depth_unknown.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "escalate.depth: Input should be 'standard' or 'low'", "code": "validation_error", @@ -23,5 +18,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_invalid_confidence_band.json b/tests/fixtures/parity/canonical/review__422_invalid_confidence_band.json index 9ece217..41fcbb5 100644 --- a/tests/fixtures/parity/canonical/review__422_invalid_confidence_band.json +++ b/tests/fixtures/parity/canonical/review__422_invalid_confidence_band.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "escalate.confidence: Unknown confidence band 'certain'; use low, medium or high.", "code": "invalid_confidence_band", @@ -23,5 +18,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_invalid_verdict_label.json b/tests/fixtures/parity/canonical/review__422_invalid_verdict_label.json index ea83a57..bb12352 100644 --- a/tests/fixtures/parity/canonical/review__422_invalid_verdict_label.json +++ b/tests/fixtures/parity/canonical/review__422_invalid_verdict_label.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "escalate.verdicts: Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True.", "code": "invalid_verdict_label", @@ -24,5 +19,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_max_assessments_negative.json b/tests/fixtures/parity/canonical/review__422_max_assessments_negative.json index 0bb1242..401ea72 100644 --- a/tests/fixtures/parity/canonical/review__422_max_assessments_negative.json +++ b/tests/fixtures/parity/canonical/review__422_max_assessments_negative.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "escalate.max_assessments: Input should be greater than or equal to 0", "code": "validation_error", @@ -23,5 +18,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_max_citations_negative.json b/tests/fixtures/parity/canonical/review__422_max_citations_negative.json index dd2cde4..ff211d2 100644 --- a/tests/fixtures/parity/canonical/review__422_max_citations_negative.json +++ b/tests/fixtures/parity/canonical/review__422_max_citations_negative.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "escalate.max_citations: Input should be greater than or equal to 0", "code": "validation_error", @@ -23,5 +18,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_max_citations_too_big.json b/tests/fixtures/parity/canonical/review__422_max_citations_too_big.json index 3d8860c..4f44152 100644 --- a/tests/fixtures/parity/canonical/review__422_max_citations_too_big.json +++ b/tests/fixtures/parity/canonical/review__422_max_citations_too_big.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "escalate.max_citations: Input should be less than or equal to 20", "code": "validation_error", @@ -23,5 +18,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_max_verifications_too_big.json b/tests/fixtures/parity/canonical/review__422_max_verifications_too_big.json index cc5e5b1..aabee59 100644 --- a/tests/fixtures/parity/canonical/review__422_max_verifications_too_big.json +++ b/tests/fixtures/parity/canonical/review__422_max_verifications_too_big.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "escalate.max_verifications: Input should be less than or equal to 20", "code": "validation_error", @@ -23,5 +18,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_text_missing.json b/tests/fixtures/parity/canonical/review__422_text_missing.json index eb7b0fc..9ce0a7d 100644 --- a/tests/fixtures/parity/canonical/review__422_text_missing.json +++ b/tests/fixtures/parity/canonical/review__422_text_missing.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "text: Field required", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "missing" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_text_wrong_type.json b/tests/fixtures/parity/canonical/review__422_text_wrong_type.json index 843bab6..7f77b76 100644 --- a/tests/fixtures/parity/canonical/review__422_text_wrong_type.json +++ b/tests/fixtures/parity/canonical/review__422_text_wrong_type.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "text: Input should be a valid string", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "string_type" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_unknown_escalate_field.json b/tests/fixtures/parity/canonical/review__422_unknown_escalate_field.json index 74bc52a..68067d4 100644 --- a/tests/fixtures/parity/canonical/review__422_unknown_escalate_field.json +++ b/tests/fixtures/parity/canonical/review__422_unknown_escalate_field.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "escalate.nope: Extra inputs are not permitted", "code": "validation_error", @@ -20,5 +15,10 @@ "type": "extra_forbidden" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_unknown_top_level_field.json b/tests/fixtures/parity/canonical/review__422_unknown_top_level_field.json index eecb052..50ccb4b 100644 --- a/tests/fixtures/parity/canonical/review__422_unknown_top_level_field.json +++ b/tests/fixtures/parity/canonical/review__422_unknown_top_level_field.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "max_verifications: Extra inputs are not permitted", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "extra_forbidden" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_unsupported_language.json b/tests/fixtures/parity/canonical/review__422_unsupported_language.json index 9382428..8a0ee14 100644 --- a/tests/fixtures/parity/canonical/review__422_unsupported_language.json +++ b/tests/fixtures/parity/canonical/review__422_unsupported_language.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", "code": "unsupported_language", @@ -18,5 +13,10 @@ "type": "unsupported_language" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_visibility_public.json b/tests/fixtures/parity/canonical/review__422_visibility_public.json index 31600c2..f266df2 100644 --- a/tests/fixtures/parity/canonical/review__422_visibility_public.json +++ b/tests/fixtures/parity/canonical/review__422_visibility_public.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "visibility: Input should be 'private' or 'unlisted'", "code": "validation_error", @@ -22,5 +17,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__422_webhook_secret_missing.json b/tests/fixtures/parity/canonical/review__422_webhook_secret_missing.json new file mode 100644 index 0000000..3286c6c --- /dev/null +++ b/tests/fixtures/parity/canonical/review__422_webhook_secret_missing.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "body": { + "detail": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "code": "webhook_secret_missing", + "errors": [ + { + "loc": [ + "body", + "webhook_url" + ], + "msg": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "type": "webhook_secret_missing" + } + ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/review__429_extract_daily_limit.json b/tests/fixtures/parity/canonical/review__429_extract_daily_limit.json index 320069f..7ea7dc9 100644 --- a/tests/fixtures/parity/canonical/review__429_extract_daily_limit.json +++ b/tests/fixtures/parity/canonical/review__429_extract_daily_limit.json @@ -1,11 +1,5 @@ { "status": 429, - "headers": { - "Content-Type": "application/json", - "Retry-After": "60", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Daily /extract limit of 1000 reached for this account. A review of a URL reads the page like /extract and counts as one /extract call.", "code": "extract_daily_limit", @@ -13,5 +7,11 @@ "retry_after": 60, "docs_url": "https://lenz.io/docs/errors#rate-limits", "upgrade_url": "https://lenz.io/plans" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__429_review_in_flight.json b/tests/fixtures/parity/canonical/review__429_review_in_flight.json index 61295d1..906f29c 100644 --- a/tests/fixtures/parity/canonical/review__429_review_in_flight.json +++ b/tests/fixtures/parity/canonical/review__429_review_in_flight.json @@ -1,14 +1,14 @@ { "status": 429, + "body": { + "detail": "This account already has 3 reviews running. Retry when one completes.", + "code": "review_in_flight", + "retry_after": 60 + }, "headers": { "Content-Type": "application/json", "Retry-After": "60", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "This account already has 3 reviews running. Retry when one completes.", - "code": "review_in_flight", - "retry_after": 60 } } diff --git a/tests/fixtures/parity/canonical/review__503_capacity.json b/tests/fixtures/parity/canonical/review__503_capacity.json index bcff7db..63b5d1c 100644 --- a/tests/fixtures/parity/canonical/review__503_capacity.json +++ b/tests/fixtures/parity/canonical/review__503_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 90, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__503_citations_capacity.json b/tests/fixtures/parity/canonical/review__503_citations_capacity.json index bcff7db..63b5d1c 100644 --- a/tests/fixtures/parity/canonical/review__503_citations_capacity.json +++ b/tests/fixtures/parity/canonical/review__503_citations_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 90, "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__delete_not_a_route.json b/tests/fixtures/parity/canonical/review__delete_not_a_route.json new file mode 100644 index 0000000..7b3ec88 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__delete_not_a_route.json @@ -0,0 +1,12 @@ +{ + "status": 405, + "body": { + "detail": "Method not allowed.", + "code": "method_not_allowed" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/review__get_403_insufficient_scope.json b/tests/fixtures/parity/canonical/review__get_403_insufficient_scope.json index 80c529c..b6b8f4f 100644 --- a/tests/fixtures/parity/canonical/review__get_403_insufficient_scope.json +++ b/tests/fixtures/parity/canonical/review__get_403_insufficient_scope.json @@ -1,14 +1,14 @@ { "status": 403, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "history:read" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"history:read\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "The access token does not allow this call.", - "code": "insufficient_scope", - "scope": "history:read" } } diff --git a/tests/fixtures/parity/canonical/review__get_404_not_found.json b/tests/fixtures/parity/canonical/review__get_404_not_found.json index 8045cd8..e73d347 100644 --- a/tests/fixtures/parity/canonical/review__get_404_not_found.json +++ b/tests/fixtures/parity/canonical/review__get_404_not_found.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Review not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Review not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/review__get_404_other_account.json b/tests/fixtures/parity/canonical/review__get_404_other_account.json index 8045cd8..e73d347 100644 --- a/tests/fixtures/parity/canonical/review__get_404_other_account.json +++ b/tests/fixtures/parity/canonical/review__get_404_other_account.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Review not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Review not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/review__get_410_purged.json b/tests/fixtures/parity/canonical/review__get_410_purged.json index bedf6a5..396b15a 100644 --- a/tests/fixtures/parity/canonical/review__get_410_purged.json +++ b/tests/fixtures/parity/canonical/review__get_410_purged.json @@ -1,13 +1,13 @@ { "status": 410, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This review is no longer available: its account removes content after a set period.", "code": "purged", "purged_at": "2026-10-01T09:07:00.123456Z" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_422_unknown_view.json b/tests/fixtures/parity/canonical/review__get_422_unknown_view.json index a9df405..70ef8e8 100644 --- a/tests/fixtures/parity/canonical/review__get_422_unknown_view.json +++ b/tests/fixtures/parity/canonical/review__get_422_unknown_view.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "view: use full or issues.", "code": "validation_error", @@ -18,5 +13,10 @@ "type": "validation_error" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_assessing.json b/tests/fixtures/parity/canonical/review__get_assessing.json index 1b8bc1d..d43f14f 100644 --- a/tests/fixtures/parity/canonical/review__get_assessing.json +++ b/tests/fixtures/parity/canonical/review__get_assessing.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "assessing", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -103,5 +98,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_assessment_rows_full_fields.json b/tests/fixtures/parity/canonical/review__get_assessment_rows_full_fields.json index 8680925..6b80bb6 100644 --- a/tests/fixtures/parity/canonical/review__get_assessment_rows_full_fields.json +++ b/tests/fixtures/parity/canonical/review__get_assessment_rows_full_fields.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -165,5 +160,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_clean.json b/tests/fixtures/parity/canonical/review__get_citations_clean.json index 332658b..1b3c2fa 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_clean.json +++ b/tests/fixtures/parity/canonical/review__get_citations_clean.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -178,5 +173,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_failed_and_unchecked.json b/tests/fixtures/parity/canonical/review__get_citations_failed_and_unchecked.json index b180507..37a0c3c 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_failed_and_unchecked.json +++ b/tests/fixtures/parity/canonical/review__get_citations_failed_and_unchecked.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -196,5 +191,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_issue_full.json b/tests/fixtures/parity/canonical/review__get_citations_issue_full.json index b3243d6..a1f8a3d 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_issue_full.json +++ b/tests/fixtures/parity/canonical/review__get_citations_issue_full.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -200,5 +195,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_issue_issues_view.json b/tests/fixtures/parity/canonical/review__get_citations_issue_issues_view.json index 9f60f10..a0c4e66 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_issue_issues_view.json +++ b/tests/fixtures/parity/canonical/review__get_citations_issue_issues_view.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "issues", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -90,5 +85,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_limit_exactly_at_limit.json b/tests/fixtures/parity/canonical/review__get_citations_limit_exactly_at_limit.json index a81b16f..2ea4bba 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_limit_exactly_at_limit.json +++ b/tests/fixtures/parity/canonical/review__get_citations_limit_exactly_at_limit.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -178,5 +173,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_limit_reached.json b/tests/fixtures/parity/canonical/review__get_citations_limit_reached.json index 33eeb1b..2c9db6a 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_limit_reached.json +++ b/tests/fixtures/parity/canonical/review__get_citations_limit_reached.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -152,5 +147,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_no_claim_but_citations.json b/tests/fixtures/parity/canonical/review__get_citations_no_claim_but_citations.json index fbb5a29..d0e9582 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_no_claim_but_citations.json +++ b/tests/fixtures/parity/canonical/review__get_citations_no_claim_but_citations.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -149,5 +144,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_no_claim_no_citation.json b/tests/fixtures/parity/canonical/review__get_citations_no_claim_no_citation.json index e9cdd01..651f5ec 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_no_claim_no_citation.json +++ b/tests/fixtures/parity/canonical/review__get_citations_no_claim_no_citation.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -66,5 +61,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_not_asked.json b/tests/fixtures/parity/canonical/review__get_citations_not_asked.json index 27d5c36..4c5445d 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_not_asked.json +++ b/tests/fixtures/parity/canonical/review__get_citations_not_asked.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -95,5 +90,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_only.json b/tests/fixtures/parity/canonical/review__get_citations_only.json index 4a86304..f642ad3 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_only.json +++ b/tests/fixtures/parity/canonical/review__get_citations_only.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -156,5 +151,10 @@ } ], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_partly_supported.json b/tests/fixtures/parity/canonical/review__get_citations_partly_supported.json index 44488cd..90d39af 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_partly_supported.json +++ b/tests/fixtures/parity/canonical/review__get_citations_partly_supported.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -178,5 +173,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_running.json b/tests/fixtures/parity/canonical/review__get_citations_running.json index ff45b91..65040d3 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_running.json +++ b/tests/fixtures/parity/canonical/review__get_citations_running.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "verifying", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -162,5 +157,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_skipped_switched_off.json b/tests/fixtures/parity/canonical/review__get_citations_skipped_switched_off.json index acfa95b..d7c7c4a 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_skipped_switched_off.json +++ b/tests/fixtures/parity/canonical/review__get_citations_skipped_switched_off.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -95,5 +90,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_citations_skipped_url_input.json b/tests/fixtures/parity/canonical/review__get_citations_skipped_url_input.json index 5919306..4cd763c 100644 --- a/tests/fixtures/parity/canonical/review__get_citations_skipped_url_input.json +++ b/tests/fixtures/parity/canonical/review__get_citations_skipped_url_input.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -66,5 +61,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_claim_limit_not_reached.json b/tests/fixtures/parity/canonical/review__get_claim_limit_not_reached.json index 548c3c8..1e92f55 100644 --- a/tests/fixtures/parity/canonical/review__get_claim_limit_not_reached.json +++ b/tests/fixtures/parity/canonical/review__get_claim_limit_not_reached.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -123,5 +118,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_claim_limit_reached_exact.json b/tests/fixtures/parity/canonical/review__get_claim_limit_reached_exact.json index 6432f12..1d1c6fc 100644 --- a/tests/fixtures/parity/canonical/review__get_claim_limit_reached_exact.json +++ b/tests/fixtures/parity/canonical/review__get_claim_limit_reached_exact.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -123,5 +118,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_completed_assess_only.json b/tests/fixtures/parity/canonical/review__get_completed_assess_only.json index 107ee44..2f25c1e 100644 --- a/tests/fixtures/parity/canonical/review__get_completed_assess_only.json +++ b/tests/fixtures/parity/canonical/review__get_completed_assess_only.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -149,5 +144,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_completed_clean.json b/tests/fixtures/parity/canonical/review__get_completed_clean.json index 0849314..3bf1368 100644 --- a/tests/fixtures/parity/canonical/review__get_completed_clean.json +++ b/tests/fixtures/parity/canonical/review__get_completed_clean.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -123,5 +118,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_completed_issues_all_verified.json b/tests/fixtures/parity/canonical/review__get_completed_issues_all_verified.json index 1b82d0e..015cbce 100644 --- a/tests/fixtures/parity/canonical/review__get_completed_issues_all_verified.json +++ b/tests/fixtures/parity/canonical/review__get_completed_issues_all_verified.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -64,10 +59,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -87,10 +82,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b79019e4", + "verification_id": "4d44dfaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", - "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "verification_url": "https://lenz.io/api/v1/verifications/4d44dfaf", + "url": "https://lenz.io/c/the-earth-is-round-4d44dfaf", "escalation": { "matched_rules": [ "verdict" @@ -138,8 +133,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "c010dca9a43543f518e49f4ec7842ceb", - "verification_id": "b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The EU AI Act took effect in March 2024.", "language": "en", "visibility": "private", @@ -155,10 +150,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:49:00.123456+00:00", - "completed_at": "2026-10-01T09:56:00.123456+00:00", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -193,8 +188,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "4e02e88b137711f18ba7758af5c40837", - "verification_id": "b79019e4", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "verification_id": "4d44dfaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -210,10 +205,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:49:00.123456+00:00", - "completed_at": "2026-10-01T09:56:00.123456+00:00", - "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", - "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/4d44dfaf", + "url": "https://lenz.io/c/the-earth-is-round-4d44dfaf", "failure": null }, "suggested_edits": null @@ -252,5 +247,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_completed_issues_full.json b/tests/fixtures/parity/canonical/review__get_completed_issues_full.json index ff3d65d..4e497ee 100644 --- a/tests/fixtures/parity/canonical/review__get_completed_issues_full.json +++ b/tests/fixtures/parity/canonical/review__get_completed_issues_full.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -64,10 +59,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -159,8 +154,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "7df4009bc27aab64ce5d5a01fbafe599", - "verification_id": "b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The EU AI Act took effect in March 2024.", "language": "en", "visibility": "private", @@ -176,10 +171,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:42:00.123456+00:00", - "completed_at": "2026-10-01T09:49:00.123456+00:00", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -240,7 +235,7 @@ }, "verification": { "verification_id": null, - "task_id": "4634b89821fd877f1edf8aaeb82b7ee5", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", "claim": null, "language": null, "visibility": null, @@ -305,5 +300,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_completed_issues_view_issues.json b/tests/fixtures/parity/canonical/review__get_completed_issues_view_issues.json index a136287..73cb5d3 100644 --- a/tests/fixtures/parity/canonical/review__get_completed_issues_view_issues.json +++ b/tests/fixtures/parity/canonical/review__get_completed_issues_view_issues.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "issues", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -64,10 +59,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -132,5 +127,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_completed_positions.json b/tests/fixtures/parity/canonical/review__get_completed_positions.json index 7a3c9e1..94542c7 100644 --- a/tests/fixtures/parity/canonical/review__get_completed_positions.json +++ b/tests/fixtures/parity/canonical/review__get_completed_positions.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -135,5 +130,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_completed_recovered_claim.json b/tests/fixtures/parity/canonical/review__get_completed_recovered_claim.json index aafdbe0..00ba6b6 100644 --- a/tests/fixtures/parity/canonical/review__get_completed_recovered_claim.json +++ b/tests/fixtures/parity/canonical/review__get_completed_recovered_claim.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -95,5 +90,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_completed_with_untraced_claim.json b/tests/fixtures/parity/canonical/review__get_completed_with_untraced_claim.json index 64ad9f0..39a95cd 100644 --- a/tests/fixtures/parity/canonical/review__get_completed_with_untraced_claim.json +++ b/tests/fixtures/parity/canonical/review__get_completed_with_untraced_claim.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -135,5 +130,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_during_quick_check.json b/tests/fixtures/parity/canonical/review__get_during_quick_check.json index 06f27e1..ecff88a 100644 --- a/tests/fixtures/parity/canonical/review__get_during_quick_check.json +++ b/tests/fixtures/parity/canonical/review__get_during_quick_check.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "assessing", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -83,5 +78,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_failed_every_assessment_failed.json b/tests/fixtures/parity/canonical/review__get_failed_every_assessment_failed.json index 93ea710..b66ecb1 100644 --- a/tests/fixtures/parity/canonical/review__get_failed_every_assessment_failed.json +++ b/tests/fixtures/parity/canonical/review__get_failed_every_assessment_failed.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -155,5 +150,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_failed_no_claim.json b/tests/fixtures/parity/canonical/review__get_failed_no_claim.json index d8506ac..a21cf53 100644 --- a/tests/fixtures/parity/canonical/review__get_failed_no_claim.json +++ b/tests/fixtures/parity/canonical/review__get_failed_no_claim.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -66,5 +61,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_failed_no_claim_untraced.json b/tests/fixtures/parity/canonical/review__get_failed_no_claim_untraced.json index 9d2e2ea..776ba8a 100644 --- a/tests/fixtures/parity/canonical/review__get_failed_no_claim_untraced.json +++ b/tests/fixtures/parity/canonical/review__get_failed_no_claim_untraced.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -66,5 +61,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_more_claims.json b/tests/fixtures/parity/canonical/review__get_more_claims.json index ffaacb2..798f568 100644 --- a/tests/fixtures/parity/canonical/review__get_more_claims.json +++ b/tests/fixtures/parity/canonical/review__get_more_claims.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -148,5 +143,10 @@ } ], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json index 62d9316..7f34405 100644 --- a/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -64,10 +59,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -115,8 +110,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "7df4009bc27aab64ce5d5a01fbafe599", - "verification_id": "b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -134,8 +129,8 @@ ], "created_at": "2026-10-01T23:55:00.123456+00:00", "completed_at": "2026-10-02T00:03:00.123456+00:00", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -146,5 +141,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_same_day_hours_apart.json index 1aeb435..8295eb7 100644 --- a/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/canonical/review__get_nested_verification_modified_at_same_day_hours_apart.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -64,10 +59,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -115,8 +110,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "7df4009bc27aab64ce5d5a01fbafe599", - "verification_id": "b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -134,8 +129,8 @@ ], "created_at": "2026-10-01T08:00:00.123456+00:00", "completed_at": "2026-10-01T20:00:00.123456+00:00", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -146,5 +141,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_policy_resolved.json b/tests/fixtures/parity/canonical/review__get_policy_resolved.json index cc1e568..7d2074b 100644 --- a/tests/fixtures/parity/canonical/review__get_policy_resolved.json +++ b/tests/fixtures/parity/canonical/review__get_policy_resolved.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -93,5 +88,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_queued.json b/tests/fixtures/parity/canonical/review__get_queued.json index 1b08c33..70a4825 100644 --- a/tests/fixtures/parity/canonical/review__get_queued.json +++ b/tests/fixtures/parity/canonical/review__get_queued.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "queued", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -59,5 +54,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_suggested_edits.json b/tests/fixtures/parity/canonical/review__get_suggested_edits.json index 8f4e0a0..0bfdfa3 100644 --- a/tests/fixtures/parity/canonical/review__get_suggested_edits.json +++ b/tests/fixtures/parity/canonical/review__get_suggested_edits.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "verifying", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -64,10 +59,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "7c83247c", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", - "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -158,8 +153,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "9bca1e0819de60a750186e812dc58e2b", - "verification_id": "7c83247c", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -175,10 +170,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:35:00.123456+00:00", - "completed_at": "2026-10-01T09:42:00.123456+00:00", - "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", - "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": { @@ -262,5 +257,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_suggested_edits_issues_view.json b/tests/fixtures/parity/canonical/review__get_suggested_edits_issues_view.json index 8914521..5d39a66 100644 --- a/tests/fixtures/parity/canonical/review__get_suggested_edits_issues_view.json +++ b/tests/fixtures/parity/canonical/review__get_suggested_edits_issues_view.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "issues", "status": "verifying", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -64,10 +59,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "7c83247c", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", - "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -125,5 +120,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_url_review.json b/tests/fixtures/parity/canonical/review__get_url_review.json index 90ea98e..9dabe53 100644 --- a/tests/fixtures/parity/canonical/review__get_url_review.json +++ b/tests/fixtures/parity/canonical/review__get_url_review.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -101,5 +96,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__get_verifying.json b/tests/fixtures/parity/canonical/review__get_verifying.json index 1663113..f49c966 100644 --- a/tests/fixtures/parity/canonical/review__get_verifying.json +++ b/tests/fixtures/parity/canonical/review__get_verifying.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "verifying", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -172,5 +167,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__idempotency_body_mismatch_422.json b/tests/fixtures/parity/canonical/review__idempotency_body_mismatch_422.json index e20089e..8cdd4be 100644 --- a/tests/fixtures/parity/canonical/review__idempotency_body_mismatch_422.json +++ b/tests/fixtures/parity/canonical/review__idempotency_body_mismatch_422.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body.", - "code": "idempotency_body_mismatch" } } diff --git a/tests/fixtures/parity/canonical/review__idempotency_conflict_409.json b/tests/fixtures/parity/canonical/review__idempotency_conflict_409.json index 8bbdff7..e0a63b2 100644 --- a/tests/fixtures/parity/canonical/review__idempotency_conflict_409.json +++ b/tests/fixtures/parity/canonical/review__idempotency_conflict_409.json @@ -1,13 +1,13 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "A review with this Idempotency-Key is still being created. Retry shortly.", "code": "idempotency_conflict", "review_id": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__idempotency_conflict_409_existing_review.json b/tests/fixtures/parity/canonical/review__idempotency_conflict_409_existing_review.json index edef440..e91fe17 100644 --- a/tests/fixtures/parity/canonical/review__idempotency_conflict_409_existing_review.json +++ b/tests/fixtures/parity/canonical/review__idempotency_conflict_409_existing_review.json @@ -1,13 +1,13 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "A review with this Idempotency-Key is still being created. Retry shortly.", "code": "idempotency_conflict", "review_id": "d6b2bd72" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/review__idempotent_replay_202.json b/tests/fixtures/parity/canonical/review__idempotent_replay_202.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/canonical/review__idempotent_replay_202.json +++ b/tests/fixtures/parity/canonical/review__idempotent_replay_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/review__receipt_202.json b/tests/fixtures/parity/canonical/review__receipt_202.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/canonical/review__receipt_202.json +++ b/tests/fixtures/parity/canonical/review__receipt_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/review__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/canonical/review__receipt_202_empty_webhook_url.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/canonical/review__receipt_202_empty_webhook_url.json +++ b/tests/fixtures/parity/canonical/review__receipt_202_empty_webhook_url.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/review__receipt_202_webhook_url.json b/tests/fixtures/parity/canonical/review__receipt_202_webhook_url.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/canonical/review__receipt_202_webhook_url.json +++ b/tests/fixtures/parity/canonical/review__receipt_202_webhook_url.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/review__replay_later_202.json b/tests/fixtures/parity/canonical/review__replay_later_202.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/canonical/review__replay_later_202.json +++ b/tests/fixtures/parity/canonical/review__replay_later_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/verify__batch_202.json b/tests/fixtures/parity/canonical/verify__batch_202.json index be544f2..654ab00 100644 --- a/tests/fixtures/parity/canonical/verify__batch_202.json +++ b/tests/fixtures/parity/canonical/verify__batch_202.json @@ -1,21 +1,21 @@ { "status": 202, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "batch_id": "7ea9cdbd4064e3cf", "items": [ { - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "claim": "The Earth is round." }, { - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", "claim": "Water boils at 100C at sea level." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__batch_capacity_503.json b/tests/fixtures/parity/canonical/verify__batch_capacity_503.json index 2e25ef0..0994e62 100644 --- a/tests/fixtures/parity/canonical/verify__batch_capacity_503.json +++ b/tests/fixtures/parity/canonical/verify__batch_capacity_503.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": "", "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__batch_empty_422.json b/tests/fixtures/parity/canonical/verify__batch_empty_422.json index 0082c60..59dd9ca 100644 --- a/tests/fixtures/parity/canonical/verify__batch_empty_422.json +++ b/tests/fixtures/parity/canonical/verify__batch_empty_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claims is required and must be non-empty.", "code": "validation_error", @@ -18,5 +13,10 @@ "type": "validation_error" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__batch_idempotency_conflict_409.json b/tests/fixtures/parity/canonical/verify__batch_idempotency_conflict_409.json index 788b394..dc881a8 100644 --- a/tests/fixtures/parity/canonical/verify__batch_idempotency_conflict_409.json +++ b/tests/fixtures/parity/canonical/verify__batch_idempotency_conflict_409.json @@ -1,13 +1,13 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "A request with this Idempotency-Key is already in progress.", "code": "idempotency_conflict", "task_id": "7cff18d" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__batch_item_missing_claim_422.json b/tests/fixtures/parity/canonical/verify__batch_item_missing_claim_422.json index 7017bcc..0e736d5 100644 --- a/tests/fixtures/parity/canonical/verify__batch_item_missing_claim_422.json +++ b/tests/fixtures/parity/canonical/verify__batch_item_missing_claim_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claims[1].claim: Field required", "code": "validation_error", @@ -21,5 +16,10 @@ "type": "missing" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__batch_no_credits_402.json b/tests/fixtures/parity/canonical/verify__batch_no_credits_402.json index 8991dc1..216b4ba 100644 --- a/tests/fixtures/parity/canonical/verify__batch_no_credits_402.json +++ b/tests/fixtures/parity/canonical/verify__batch_no_credits_402.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Insufficient credits for batch.", "code": "no_credits", @@ -13,8 +8,13 @@ "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "requested": 1, "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 10 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__batch_partial_202.json b/tests/fixtures/parity/canonical/verify__batch_partial_202.json index 1e3656f..f3bb45d 100644 --- a/tests/fixtures/parity/canonical/verify__batch_partial_202.json +++ b/tests/fixtures/parity/canonical/verify__batch_partial_202.json @@ -1,18 +1,18 @@ { "status": 202, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "batch_id": "7ea9cdbd4064e3cf", "items": [ { - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "claim": "First claim." } ], "partial": true + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__batch_too_many_422.json b/tests/fixtures/parity/canonical/verify__batch_too_many_422.json index 5105df7..b239559 100644 --- a/tests/fixtures/parity/canonical/verify__batch_too_many_422.json +++ b/tests/fixtures/parity/canonical/verify__batch_too_many_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Batch size exceeds maximum of 20.", "code": "too_many_items", @@ -18,5 +13,10 @@ "type": "too_many_items" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__blank_claim_422.json b/tests/fixtures/parity/canonical/verify__blank_claim_422.json index a5a1062..bfcc575 100644 --- a/tests/fixtures/parity/canonical/verify__blank_claim_422.json +++ b/tests/fixtures/parity/canonical/verify__blank_claim_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claim is required.", "code": "blank_input", @@ -18,5 +13,10 @@ "type": "blank_input" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__capacity_503.json b/tests/fixtures/parity/canonical/verify__capacity_503.json index 2e25ef0..0994e62 100644 --- a/tests/fixtures/parity/canonical/verify__capacity_503.json +++ b/tests/fixtures/parity/canonical/verify__capacity_503.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": "", "docs_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__delete_200.json b/tests/fixtures/parity/canonical/verify__delete_200.json new file mode 100644 index 0000000..cddadae --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__delete_200.json @@ -0,0 +1,11 @@ +{ + "status": 200, + "body": { + "ok": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/verify__delete_404.json b/tests/fixtures/parity/canonical/verify__delete_404.json new file mode 100644 index 0000000..775d847 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__delete_404.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "body": { + "detail": "Not found.", + "code": "not_found" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/verify__delete_not_yours_404.json b/tests/fixtures/parity/canonical/verify__delete_not_yours_404.json new file mode 100644 index 0000000..775d847 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__delete_not_yours_404.json @@ -0,0 +1,12 @@ +{ + "status": 404, + "body": { + "detail": "Not found.", + "code": "not_found" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/verify__idempotency_body_mismatch_422.json b/tests/fixtures/parity/canonical/verify__idempotency_body_mismatch_422.json index e20089e..8cdd4be 100644 --- a/tests/fixtures/parity/canonical/verify__idempotency_body_mismatch_422.json +++ b/tests/fixtures/parity/canonical/verify__idempotency_body_mismatch_422.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body.", + "code": "idempotency_body_mismatch" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body.", - "code": "idempotency_body_mismatch" } } diff --git a/tests/fixtures/parity/canonical/verify__idempotency_conflict_409.json b/tests/fixtures/parity/canonical/verify__idempotency_conflict_409.json index 1f8ddf5..87958d2 100644 --- a/tests/fixtures/parity/canonical/verify__idempotency_conflict_409.json +++ b/tests/fixtures/parity/canonical/verify__idempotency_conflict_409.json @@ -1,13 +1,13 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "A request with this Idempotency-Key is already in progress.", "code": "idempotency_conflict", "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__idempotency_key_replay.json b/tests/fixtures/parity/canonical/verify__idempotency_key_replay.json index b510371..d47aeaf 100644 --- a/tests/fixtures/parity/canonical/verify__idempotency_key_replay.json +++ b/tests/fixtures/parity/canonical/verify__idempotency_key_replay.json @@ -1,12 +1,12 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/verify__implicit_dedup_200.json b/tests/fixtures/parity/canonical/verify__implicit_dedup_200.json index b510371..d47aeaf 100644 --- a/tests/fixtures/parity/canonical/verify__implicit_dedup_200.json +++ b/tests/fixtures/parity/canonical/verify__implicit_dedup_200.json @@ -1,12 +1,12 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/verify__implicit_repeat_replay.json b/tests/fixtures/parity/canonical/verify__implicit_repeat_replay.json index b510371..d47aeaf 100644 --- a/tests/fixtures/parity/canonical/verify__implicit_repeat_replay.json +++ b/tests/fixtures/parity/canonical/verify__implicit_repeat_replay.json @@ -1,12 +1,12 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/verify__invalid_depth_422.json b/tests/fixtures/parity/canonical/verify__invalid_depth_422.json index ae4883c..3cf1746 100644 --- a/tests/fixtures/parity/canonical/verify__invalid_depth_422.json +++ b/tests/fixtures/parity/canonical/verify__invalid_depth_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "depth: Input should be 'standard' or 'low'", "code": "validation_error", @@ -22,5 +17,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__invalid_language_422.json b/tests/fixtures/parity/canonical/verify__invalid_language_422.json index 5e82c5f..7025ff2 100644 --- a/tests/fixtures/parity/canonical/verify__invalid_language_422.json +++ b/tests/fixtures/parity/canonical/verify__invalid_language_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", "code": "unsupported_language", @@ -18,5 +13,10 @@ "type": "unsupported_language" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__list_200.json b/tests/fixtures/parity/canonical/verify__list_200.json index 0b7e73b..335daa4 100644 --- a/tests/fixtures/parity/canonical/verify__list_200.json +++ b/tests/fixtures/parity/canonical/verify__list_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -18,12 +13,12 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "language": "en" }, { - "verification_id": "b4e2e99d", + "verification_id": "4d44dfaf", "claim": "The Earth is round.", "domain": "Science", "entities": [], @@ -33,13 +28,18 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:35:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", "language": "en" } ], "total": 2, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__list_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/verify__list_200_modified_at_crosses_midnight_by_minutes.json index 267a53e..ed48dbb 100644 --- a/tests/fixtures/parity/canonical/verify__list_200_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/canonical/verify__list_200_modified_at_crosses_midnight_by_minutes.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -26,5 +21,10 @@ "total": 1, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__list_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/verify__list_200_modified_at_same_day_hours_apart.json index 65e23bc..0c50625 100644 --- a/tests/fixtures/parity/canonical/verify__list_200_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/canonical/verify__list_200_modified_at_same_day_hours_apart.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -26,5 +21,10 @@ "total": 1, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__list_clamped_200.json b/tests/fixtures/parity/canonical/verify__list_clamped_200.json index 3d639d4..c208418 100644 --- a/tests/fixtures/parity/canonical/verify__list_clamped_200.json +++ b/tests/fixtures/parity/canonical/verify__list_clamped_200.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 0, "page": 1, "page_size": 100 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__list_empty_200.json b/tests/fixtures/parity/canonical/verify__list_empty_200.json index b419316..406be8e 100644 --- a/tests/fixtures/parity/canonical/verify__list_empty_200.json +++ b/tests/fixtures/parity/canonical/verify__list_empty_200.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 0, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__list_page2_200.json b/tests/fixtures/parity/canonical/verify__list_page2_200.json index f83290d..6008819 100644 --- a/tests/fixtures/parity/canonical/verify__list_page2_200.json +++ b/tests/fixtures/parity/canonical/verify__list_page2_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -18,13 +13,18 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "language": "en" } ], "total": 3, "page": 2, "page_size": 2 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__misnamed_field_422.json b/tests/fixtures/parity/canonical/verify__misnamed_field_422.json index 877dab6..381ab3e 100644 --- a/tests/fixtures/parity/canonical/verify__misnamed_field_422.json +++ b/tests/fixtures/parity/canonical/verify__misnamed_field_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claim: Field required", "code": "validation_error", @@ -24,5 +19,10 @@ "unrecognized_fields": [ "statement" ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__missing_claim_422.json b/tests/fixtures/parity/canonical/verify__missing_claim_422.json index 9136471..f5b9868 100644 --- a/tests/fixtures/parity/canonical/verify__missing_claim_422.json +++ b/tests/fixtures/parity/canonical/verify__missing_claim_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claim: Field required", "code": "validation_error", @@ -19,5 +14,10 @@ "type": "missing" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__no_credits_402.json b/tests/fixtures/parity/canonical/verify__no_credits_402.json index 1f73a67..d93b382 100644 --- a/tests/fixtures/parity/canonical/verify__no_credits_402.json +++ b/tests/fixtures/parity/canonical/verify__no_credits_402.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining claim checks.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 10 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__replay_later_202.json b/tests/fixtures/parity/canonical/verify__replay_later_202.json index b6dbf5e..1991e8a 100644 --- a/tests/fixtures/parity/canonical/verify__replay_later_202.json +++ b/tests/fixtures/parity/canonical/verify__replay_later_202.json @@ -1,13 +1,13 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "99a0a9ca89e1e72c" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued", - "chain_id": "78ac4e9f234ea3bd" } } diff --git a/tests/fixtures/parity/canonical/verify__select_202.json b/tests/fixtures/parity/canonical/verify__select_202.json index be544f2..654ab00 100644 --- a/tests/fixtures/parity/canonical/verify__select_202.json +++ b/tests/fixtures/parity/canonical/verify__select_202.json @@ -1,21 +1,21 @@ { "status": 202, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "batch_id": "7ea9cdbd4064e3cf", "items": [ { - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "claim": "The Earth is round." }, { - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", "claim": "Water boils at 100C at sea level." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__select_empty_422.json b/tests/fixtures/parity/canonical/verify__select_empty_422.json index 8651afb..bab491b 100644 --- a/tests/fixtures/parity/canonical/verify__select_empty_422.json +++ b/tests/fixtures/parity/canonical/verify__select_empty_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "claims is required.", "code": "blank_input", @@ -18,5 +13,10 @@ "type": "blank_input" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__select_invalid_selection_422.json b/tests/fixtures/parity/canonical/verify__select_invalid_selection_422.json index d8f6dd6..7099c4a 100644 --- a/tests/fixtures/parity/canonical/verify__select_invalid_selection_422.json +++ b/tests/fixtures/parity/canonical/verify__select_invalid_selection_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Selected text was not one of the offered claims: 'The Moon is made of cheese.'", "code": "invalid_selection", @@ -18,5 +13,10 @@ "type": "invalid_selection" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__select_no_selection_pending_409.json b/tests/fixtures/parity/canonical/verify__select_no_selection_pending_409.json index 149402e..47d776a 100644 --- a/tests/fixtures/parity/canonical/verify__select_no_selection_pending_409.json +++ b/tests/fixtures/parity/canonical/verify__select_no_selection_pending_409.json @@ -1,12 +1,12 @@ { "status": 409, + "body": { + "detail": "This task has no pending claim selection.", + "code": "no_selection_pending" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "This task has no pending claim selection.", - "code": "no_selection_pending" } } diff --git a/tests/fixtures/parity/canonical/verify__select_too_many_422.json b/tests/fixtures/parity/canonical/verify__select_too_many_422.json index db644a3..cf16deb 100644 --- a/tests/fixtures/parity/canonical/verify__select_too_many_422.json +++ b/tests/fixtures/parity/canonical/verify__select_too_many_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Cannot select more than 20 claims.", "code": "too_many_items", @@ -18,5 +13,10 @@ "type": "too_many_items" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__select_unknown_task_404.json b/tests/fixtures/parity/canonical/verify__select_unknown_task_404.json index ed0bc13..8c0eaee 100644 --- a/tests/fixtures/parity/canonical/verify__select_unknown_task_404.json +++ b/tests/fixtures/parity/canonical/verify__select_unknown_task_404.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Task not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Task not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json index 799ffa9..40ec53e 100644 --- a/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json +++ b/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#cancelled" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_completed.json b/tests/fixtures/parity/canonical/verify__status_completed.json index c0ae291..4cdad25 100644 --- a/tests/fixtures/parity/canonical/verify__status_completed.json +++ b/tests/fixtures/parity/canonical/verify__status_completed.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -30,8 +25,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:21:00.123456+00:00", - "completed_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_completed_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while.json index c0ae291..4cdad25 100644 --- a/tests/fixtures/parity/canonical/verify__status_completed_after_a_while.json +++ b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -30,8 +25,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:21:00.123456+00:00", - "completed_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json index 64eb71f..df91232 100644 --- a/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json index 2cad36e..7d239be 100644 --- a/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/canonical/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/verify__status_completed_modified_at_crosses_midnight_by_minutes.json index 64eb71f..df91232 100644 --- a/tests/fixtures/parity/canonical/verify__status_completed_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/canonical/verify__status_completed_modified_at_crosses_midnight_by_minutes.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/verify__status_completed_modified_at_same_day_hours_apart.json index 2cad36e..7d239be 100644 --- a/tests/fixtures/parity/canonical/verify__status_completed_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/canonical/verify__status_completed_modified_at_same_day_hours_apart.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_failed.json b/tests/fixtures/parity/canonical/verify__status_failed.json index f0674e2..cc029a3 100644 --- a/tests/fixtures/parity/canonical/verify__status_failed.json +++ b/tests/fixtures/parity/canonical/verify__status_failed.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_failed_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_failed_after_a_while.json index 7deb947..c3694b4 100644 --- a/tests/fixtures/parity/canonical/verify__status_failed_after_a_while.json +++ b/tests/fixtures/parity/canonical/verify__status_failed_after_a_while.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#internal" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_failed_after_a_while_framing.json b/tests/fixtures/parity/canonical/verify__status_failed_after_a_while_framing.json index 1329048..1774968 100644 --- a/tests/fixtures/parity/canonical/verify__status_failed_after_a_while_framing.json +++ b/tests/fixtures/parity/canonical/verify__status_failed_after_a_while_framing.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": true, "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_failed_retryable.json b/tests/fixtures/parity/canonical/verify__status_failed_retryable.json index b7594ed..4fafcc9 100644 --- a/tests/fixtures/parity/canonical/verify__status_failed_retryable.json +++ b/tests/fixtures/parity/canonical/verify__status_failed_retryable.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": true, "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_needs_input.json b/tests/fixtures/parity/canonical/verify__status_needs_input.json index 194a4c6..59a23c4 100644 --- a/tests/fixtures/parity/canonical/verify__status_needs_input.json +++ b/tests/fixtures/parity/canonical/verify__status_needs_input.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "needs_input", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -20,5 +15,10 @@ } ], "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_not_a_claim.json b/tests/fixtures/parity/canonical/verify__status_not_a_claim.json index dea790c..335bb1b 100644 --- a/tests/fixtures/parity/canonical/verify__status_not_a_claim.json +++ b/tests/fixtures/parity/canonical/verify__status_not_a_claim.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#invalid-input" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_not_a_claim_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_not_a_claim_after_a_while.json index d8a3007..771ab68 100644 --- a/tests/fixtures/parity/canonical/verify__status_not_a_claim_after_a_while.json +++ b/tests/fixtures/parity/canonical/verify__status_not_a_claim_after_a_while.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#invalid-input" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_not_yours_404.json b/tests/fixtures/parity/canonical/verify__status_not_yours_404.json index ed0bc13..8c0eaee 100644 --- a/tests/fixtures/parity/canonical/verify__status_not_yours_404.json +++ b/tests/fixtures/parity/canonical/verify__status_not_yours_404.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Task not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Task not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_completed.json b/tests/fixtures/parity/canonical/verify__status_older_run_completed.json index c0ae291..4cdad25 100644 --- a/tests/fixtures/parity/canonical/verify__status_older_run_completed.json +++ b/tests/fixtures/parity/canonical/verify__status_older_run_completed.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -30,8 +25,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:21:00.123456+00:00", - "completed_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json b/tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json index 3966046..2899dcb 100644 --- a/tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json +++ b/tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#internal" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json b/tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json index f0674e2..cc029a3 100644 --- a/tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json +++ b/tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json b/tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json index 8ff9472..5e5d47b 100644 --- a/tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json +++ b/tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -12,8 +7,13 @@ "step": "research", "index": 2, "total": 5, - "elapsed_seconds": 2, + "elapsed_seconds": 1, "poll_after_seconds": 5 } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_processing.json b/tests/fixtures/parity/canonical/verify__status_processing.json index 8ff9472..5e5d47b 100644 --- a/tests/fixtures/parity/canonical/verify__status_processing.json +++ b/tests/fixtures/parity/canonical/verify__status_processing.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -12,8 +7,13 @@ "step": "research", "index": 2, "total": 5, - "elapsed_seconds": 2, + "elapsed_seconds": 1, "poll_after_seconds": 5 } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_processing_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_processing_after_a_while.json index 077dee8..e0638f1 100644 --- a/tests/fixtures/parity/canonical/verify__status_processing_after_a_while.json +++ b/tests/fixtures/parity/canonical/verify__status_processing_after_a_while.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -12,8 +7,13 @@ "step": "adjudication", "index": 4, "total": 5, - "elapsed_seconds": 2, + "elapsed_seconds": 1, "poll_after_seconds": 5 } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_task_stuck.json b/tests/fixtures/parity/canonical/verify__status_task_stuck.json index 8230e8c..4ca4162 100644 --- a/tests/fixtures/parity/canonical/verify__status_task_stuck.json +++ b/tests/fixtures/parity/canonical/verify__status_task_stuck.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -16,5 +11,10 @@ "retryable": true, "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__status_unknown_404.json b/tests/fixtures/parity/canonical/verify__status_unknown_404.json index ed0bc13..8c0eaee 100644 --- a/tests/fixtures/parity/canonical/verify__status_unknown_404.json +++ b/tests/fixtures/parity/canonical/verify__status_unknown_404.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Task not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Task not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/verify__submit_202.json b/tests/fixtures/parity/canonical/verify__submit_202.json index b510371..d47aeaf 100644 --- a/tests/fixtures/parity/canonical/verify__submit_202.json +++ b/tests/fixtures/parity/canonical/verify__submit_202.json @@ -1,12 +1,12 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/verify__submit_202_options.json b/tests/fixtures/parity/canonical/verify__submit_202_options.json index b510371..d47aeaf 100644 --- a/tests/fixtures/parity/canonical/verify__submit_202_options.json +++ b/tests/fixtures/parity/canonical/verify__submit_202_options.json @@ -1,12 +1,12 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/verify__submit_202_text_alias.json b/tests/fixtures/parity/canonical/verify__submit_202_text_alias.json index b510371..d47aeaf 100644 --- a/tests/fixtures/parity/canonical/verify__submit_202_text_alias.json +++ b/tests/fixtures/parity/canonical/verify__submit_202_text_alias.json @@ -1,12 +1,12 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued" } } diff --git a/tests/fixtures/parity/canonical/verify__unauthenticated_401.json b/tests/fixtures/parity/canonical/verify__unauthenticated_401.json index 1c3df51..dcdaad0 100644 --- a/tests/fixtures/parity/canonical/verify__unauthenticated_401.json +++ b/tests/fixtures/parity/canonical/verify__unauthenticated_401.json @@ -1,12 +1,12 @@ { "status": 401, + "body": { + "detail": "Unauthorized", + "code": "not_authenticated" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized", - "code": "not_authenticated" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_200.json b/tests/fixtures/parity/canonical/verify__verification_200.json index e6d5f88..54fbdb7 100644 --- a/tests/fixtures/parity/canonical/verify__verification_200.json +++ b/tests/fixtures/parity/canonical/verify__verification_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,8 +22,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_200_covered.json b/tests/fixtures/parity/canonical/verify__verification_200_covered.json index ab1972e..c9c3df6 100644 --- a/tests/fixtures/parity/canonical/verify__verification_200_covered.json +++ b/tests/fixtures/parity/canonical/verify__verification_200_covered.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,8 +22,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -81,13 +76,18 @@ "coverage": { "status": "covered", "reasons": [], - "certificate_id": "df2c8d27663c61f5a32c2c628d182f81", - "certificate_url": "https://lenz.io/certificate/df2c8d27663c61f5a32c2c628d182f81", + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", "as_of": "2026-03-10T12:01:30+00:00", "currency": "EUR", "cap": 10000, "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_crosses_midnight_by_minutes.json index 9b01ef7..5c62918 100644 --- a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_crosses_midnight_by_minutes.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_null.json b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_null.json index 3cd999b..54fbdb7 100644 --- a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_null.json +++ b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_null.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,8 +22,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_same_day_hours_apart.json index 6a57a82..cb9d0dd 100644 --- a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_same_day_hours_apart.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_set.json b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_set.json index 9b01ef7..5c62918 100644 --- a/tests/fixtures/parity/canonical/verify__verification_200_modified_at_set.json +++ b/tests/fixtures/parity/canonical/verify__verification_200_modified_at_set.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_200_result_json_conclusion_only.json b/tests/fixtures/parity/canonical/verify__verification_200_result_json_conclusion_only.json index 68f5a3f..eb7471c 100644 --- a/tests/fixtures/parity/canonical/verify__verification_200_result_json_conclusion_only.json +++ b/tests/fixtures/parity/canonical/verify__verification_200_result_json_conclusion_only.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -20,8 +15,8 @@ "executive_summary": "The claim is verified.", "warnings": [], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [], "audit": { "adjudication_summary": "", @@ -52,5 +47,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_200_result_json_empty.json b/tests/fixtures/parity/canonical/verify__verification_200_result_json_empty.json index 68f5a3f..eb7471c 100644 --- a/tests/fixtures/parity/canonical/verify__verification_200_result_json_empty.json +++ b/tests/fixtures/parity/canonical/verify__verification_200_result_json_empty.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -20,8 +15,8 @@ "executive_summary": "The claim is verified.", "warnings": [], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [], "audit": { "adjudication_summary": "", @@ -52,5 +47,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_200_result_json_old_shape_strings.json b/tests/fixtures/parity/canonical/verify__verification_200_result_json_old_shape_strings.json index d236164..b2a825a 100644 --- a/tests/fixtures/parity/canonical/verify__verification_200_result_json_old_shape_strings.json +++ b/tests/fixtures/parity/canonical/verify__verification_200_result_json_old_shape_strings.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -20,8 +15,8 @@ "executive_summary": "The claim is verified.", "warnings": [], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "", @@ -68,5 +63,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_404.json b/tests/fixtures/parity/canonical/verify__verification_404.json index f1a8c33..775d847 100644 --- a/tests/fixtures/parity/canonical/verify__verification_404.json +++ b/tests/fixtures/parity/canonical/verify__verification_404.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_by_task_id_200.json b/tests/fixtures/parity/canonical/verify__verification_by_task_id_200.json index e6d5f88..54fbdb7 100644 --- a/tests/fixtures/parity/canonical/verify__verification_by_task_id_200.json +++ b/tests/fixtures/parity/canonical/verify__verification_by_task_id_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,8 +22,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_failed_409.json b/tests/fixtures/parity/canonical/verify__verification_failed_409.json index b7c3f82..1a8058b 100644 --- a/tests/fixtures/parity/canonical/verify__verification_failed_409.json +++ b/tests/fixtures/parity/canonical/verify__verification_failed_409.json @@ -1,10 +1,5 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This check failed and has no result.", "code": "verification_failed", @@ -19,5 +14,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" } + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_failed_409_not_a_claim_after_a_while.json b/tests/fixtures/parity/canonical/verify__verification_failed_409_not_a_claim_after_a_while.json index 9479d57..62a1cc6 100644 --- a/tests/fixtures/parity/canonical/verify__verification_failed_409_not_a_claim_after_a_while.json +++ b/tests/fixtures/parity/canonical/verify__verification_failed_409_not_a_claim_after_a_while.json @@ -1,10 +1,5 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This check failed and has no result.", "code": "verification_failed", @@ -19,5 +14,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#invalid-input" } + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_not_ready_409.json b/tests/fixtures/parity/canonical/verify__verification_not_ready_409.json index d5ace3f..dae092f 100644 --- a/tests/fixtures/parity/canonical/verify__verification_not_ready_409.json +++ b/tests/fixtures/parity/canonical/verify__verification_not_ready_409.json @@ -1,15 +1,15 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This check is still running.", "code": "verification_not_ready", "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad", "hint": "Poll GET /verify/status/7cff18da2d979d6370e54a160be121ad until it completes, then read its result." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_not_yours_404.json b/tests/fixtures/parity/canonical/verify__verification_not_yours_404.json index f1a8c33..775d847 100644 --- a/tests/fixtures/parity/canonical/verify__verification_not_yours_404.json +++ b/tests/fixtures/parity/canonical/verify__verification_not_yours_404.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_public_anonymous.json b/tests/fixtures/parity/canonical/verify__verification_public_anonymous.json index aec1fd3..9b93c51 100644 --- a/tests/fixtures/parity/canonical/verify__verification_public_anonymous.json +++ b/tests/fixtures/parity/canonical/verify__verification_public_anonymous.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,8 +22,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -79,5 +74,10 @@ }, "language": "en", "coverage": null + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_public_of_another_user.json b/tests/fixtures/parity/canonical/verify__verification_public_of_another_user.json index 4ccbda8..4727423 100644 --- a/tests/fixtures/parity/canonical/verify__verification_public_of_another_user.json +++ b/tests/fixtures/parity/canonical/verify__verification_public_of_another_user.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,8 +22,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_unknown_task_404.json b/tests/fixtures/parity/canonical/verify__verification_unknown_task_404.json index e577e2e..2833a7f 100644 --- a/tests/fixtures/parity/canonical/verify__verification_unknown_task_404.json +++ b/tests/fixtures/parity/canonical/verify__verification_unknown_task_404.json @@ -1,13 +1,13 @@ { "status": 404, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Not found.", "code": "not_found", "hint": "Unknown id. If this is a task_id, poll GET /verify/status/dddddddddddddddddddddddddddddddd for its status." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_zero_retention_expired_410.json b/tests/fixtures/parity/canonical/verify__verification_zero_retention_expired_410.json index 010b258..924d468 100644 --- a/tests/fixtures/parity/canonical/verify__verification_zero_retention_expired_410.json +++ b/tests/fixtures/parity/canonical/verify__verification_zero_retention_expired_410.json @@ -1,13 +1,13 @@ { "status": 410, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This verification is no longer available: its account removes verifications after a set period.", "code": "purged", "purged_at": "2026-10-01T09:07:00.123456+00:00" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__verification_zero_retention_served.json b/tests/fixtures/parity/canonical/verify__verification_zero_retention_served.json index fca733d..ec77f26 100644 --- a/tests/fixtures/parity/canonical/verify__verification_zero_retention_served.json +++ b/tests/fixtures/parity/canonical/verify__verification_zero_retention_served.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,8 +22,8 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", - "completed_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "NASA", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/canonical/verify__webhook_secret_missing_422.json b/tests/fixtures/parity/canonical/verify__webhook_secret_missing_422.json new file mode 100644 index 0000000..85482fc --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__webhook_secret_missing_422.json @@ -0,0 +1,22 @@ +{ + "status": 422, + "body": { + "detail": "webhook_url was supplied but this API key has no HMAC secret. Generate one at https://lenz.io/api-credentials (Webhooks panel) before submitting webhook-delivered calls.", + "code": "webhook_secret_missing", + "errors": [ + { + "loc": [ + "body", + "webhook_url" + ], + "msg": "webhook_url was supplied but this API key has no HMAC secret. Generate one at https://lenz.io/api-credentials (Webhooks panel) before submitting webhook-delivered calls.", + "type": "webhook_secret_missing" + } + ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__certificate_timestamped.json b/tests/fixtures/parity/canonical/webhook__certificate_timestamped.json index f2093db..422b1ce 100644 --- a/tests/fixtures/parity/canonical/webhook__certificate_timestamped.json +++ b/tests/fixtures/parity/canonical/webhook__certificate_timestamped.json @@ -1,17 +1,16 @@ { "status": 200, - "headers": {}, "body": { "event": "certificate.timestamped", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", "verification_id": "68ca4aaf", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", - "batch_id": null, "status": "completed", "coverage": { "status": "covered", "reasons": [], - "certificate_id": "def7232d27eda03cf261569aee9948c5", - "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", "as_of": "2026-09-03T10:00:00+00:00", "currency": "EUR", "cap": 10000, @@ -19,7 +18,7 @@ "terms_version": "v1" }, "attempt": 1, - "delivered_at": "2026-10-01T09:28:00.123456+00:00", - "event_id": "evt_0b92b443eb69b9c71434fdc6" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__certificate_timestamped_review_child.json b/tests/fixtures/parity/canonical/webhook__certificate_timestamped_review_child.json index 80582ef..0a4075b 100644 --- a/tests/fixtures/parity/canonical/webhook__certificate_timestamped_review_child.json +++ b/tests/fixtures/parity/canonical/webhook__certificate_timestamped_review_child.json @@ -1,26 +1,25 @@ { "status": 200, - "headers": {}, "body": { "event": "certificate.timestamped", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", "verification_id": "68ca4aaf", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", - "batch_id": null, + "review_id": "d6b2bd72", "status": "completed", "coverage": { "status": "covered", "reasons": [], - "certificate_id": "def7232d27eda03cf261569aee9948c5", - "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", "as_of": "2026-09-03T10:00:00+00:00", "currency": "EUR", "cap": 10000, "aggregate": 500000, "terms_version": "v1" }, - "review_id": "d6b2bd72", "attempt": 1, - "delivered_at": "2026-10-01T09:28:00.123456+00:00", - "event_id": "evt_0b92b443eb69b9c71434fdc6" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__citecheck_completed.json b/tests/fixtures/parity/canonical/webhook__citecheck_completed.json index ce60399..e5c5a3d 100644 --- a/tests/fixtures/parity/canonical/webhook__citecheck_completed.json +++ b/tests/fixtures/parity/canonical/webhook__citecheck_completed.json @@ -1,17 +1,16 @@ { "status": 200, - "headers": {}, "body": { "event": "citecheck.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "citecheck_id": "98c0e7d5", + "citecheck_id": "8eb49302", "status": "completed", "citecheck": { - "citecheck_id": "98c0e7d5", + "citecheck_id": "8eb49302", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -21,7 +20,7 @@ "citations_found": 2, "citations_selected": 2, "citation_limit": 2, - "citation_limit_reached": false, + "citation_limit_exceeded": false, "citation_checks": { "checked": 2, "unchecked": 0, @@ -128,6 +127,7 @@ "more_citations": [] }, "attempt": 1, - "delivered_at": "2026-10-01T09:42:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__citecheck_failed.json b/tests/fixtures/parity/canonical/webhook__citecheck_failed.json index 7474d1c..809b573 100644 --- a/tests/fixtures/parity/canonical/webhook__citecheck_failed.json +++ b/tests/fixtures/parity/canonical/webhook__citecheck_failed.json @@ -1,17 +1,16 @@ { "status": 200, - "headers": {}, "body": { "event": "citecheck.failed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "citecheck_id": "98c0e7d5", + "citecheck_id": "8eb49302", "status": "failed", "citecheck": { - "citecheck_id": "98c0e7d5", + "citecheck_id": "8eb49302", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -21,7 +20,7 @@ "citations_found": null, "citations_selected": null, "citation_limit": 20, - "citation_limit_reached": null, + "citation_limit_exceeded": null, "citation_checks": null, "citation_issues": 0 }, @@ -32,15 +31,17 @@ "citation_issues": [], "citation_failures": [], "failure": { - "failure_reason": "no_citations", + "code": "no_citations", + "detail": "The text holds no citation to check.", + "hint": "The text has no citation to check: no link, DOI or numbered reference with one.", "failure_class": "invalid_input", "retryable": false, - "hint": "The text has no citation to check: no link, DOI or numbered reference with one.", "docs_url": "https://lenz.io/docs/errors#invalid-input" }, "more_citations": null }, "attempt": 1, - "delivered_at": "2026-10-01T09:42:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__review_completed.json b/tests/fixtures/parity/canonical/webhook__review_completed.json index 78cf7d2..35b6ad5 100644 --- a/tests/fixtures/parity/canonical/webhook__review_completed.json +++ b/tests/fixtures/parity/canonical/webhook__review_completed.json @@ -1,18 +1,17 @@ { "status": 200, - "headers": {}, "body": { "event": "review.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "completed", "review": { - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -30,9 +29,10 @@ "suggest_edits": false }, "summary": { + "claims_found": 1, "claims_selected": 1, "claim_limit": 20, - "claim_limit_reached": false, + "claim_limit_exceeded": false, "input_truncated": false, "assessments": { "completed": 1, @@ -47,7 +47,7 @@ "citations_found": null, "citations_selected": null, "citation_limit": null, - "citation_limit_reached": null, + "citation_limit_exceeded": null, "citation_checks": null, "citation_issues": 0, "citations_skipped": null @@ -64,10 +64,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "8ceed6f0", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", - "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -99,13 +99,11 @@ "status": "completed", "verdict": "False", "confidence": "high", + "verification_url": null, "rationale": null, "dissent": null, - "verification_url": null, - "error_code": null, - "identified_claims": [], - "hint": null, "suggested_rewrite": null, + "more_claims": [], "failure": null }, "escalation": { @@ -117,8 +115,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "5ccac77f53ff49328ec134fb29d66542", - "verification_id": "8ceed6f0", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -134,10 +132,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:56:00.123456+00:00", - "modified_at": null, - "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", - "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "completed_at": "2026-10-01T09:14:00.123456+00:00", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -150,6 +148,7 @@ "more_citations": [] }, "attempt": 1, - "delivered_at": "2026-10-01T10:03:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__review_completed_key_default_url.json b/tests/fixtures/parity/canonical/webhook__review_completed_key_default_url.json index 0631d53..b1bd673 100644 --- a/tests/fixtures/parity/canonical/webhook__review_completed_key_default_url.json +++ b/tests/fixtures/parity/canonical/webhook__review_completed_key_default_url.json @@ -1,18 +1,17 @@ { "status": 200, - "headers": {}, "body": { "event": "review.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "completed", "review": { - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -30,9 +29,10 @@ "suggest_edits": false }, "summary": { + "claims_found": 1, "claims_selected": 1, "claim_limit": 20, - "claim_limit_reached": false, + "claim_limit_exceeded": false, "input_truncated": false, "assessments": { "completed": 1, @@ -47,7 +47,7 @@ "citations_found": null, "citations_selected": null, "citation_limit": null, - "citation_limit_reached": null, + "citation_limit_exceeded": null, "citation_checks": null, "citation_issues": 0, "citations_skipped": null @@ -75,13 +75,11 @@ "status": "completed", "verdict": "True", "confidence": "high", + "verification_url": null, "rationale": null, "dissent": null, - "verification_url": null, - "error_code": null, - "identified_claims": [], - "hint": null, "suggested_rewrite": null, + "more_claims": [], "failure": null }, "escalation": { @@ -99,6 +97,7 @@ "more_citations": [] }, "attempt": 1, - "delivered_at": "2026-10-01T09:42:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__review_completed_oversized_rebuilt.json b/tests/fixtures/parity/canonical/webhook__review_completed_oversized_rebuilt.json index e96d947..3f18193 100644 --- a/tests/fixtures/parity/canonical/webhook__review_completed_oversized_rebuilt.json +++ b/tests/fixtures/parity/canonical/webhook__review_completed_oversized_rebuilt.json @@ -1,10 +1,10 @@ { "status": 200, - "headers": {}, "body": { "event": "review.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "completed" - } + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__review_failed.json b/tests/fixtures/parity/canonical/webhook__review_failed.json index f382954..40f0ca5 100644 --- a/tests/fixtures/parity/canonical/webhook__review_failed.json +++ b/tests/fixtures/parity/canonical/webhook__review_failed.json @@ -1,18 +1,17 @@ { "status": 200, - "headers": {}, "body": { "event": "review.failed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "failed", "review": { - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -30,9 +29,10 @@ "suggest_edits": false }, "summary": { + "claims_found": null, "claims_selected": null, "claim_limit": 20, - "claim_limit_reached": null, + "claim_limit_exceeded": null, "input_truncated": false, "assessments": null, "verifications": null, @@ -40,7 +40,7 @@ "citations_found": null, "citations_selected": null, "citation_limit": null, - "citation_limit_reached": null, + "citation_limit_exceeded": null, "citation_checks": null, "citation_issues": 0, "citations_skipped": null @@ -56,10 +56,11 @@ "claims": [], "citations": [], "failure": { - "failure_reason": "no_claim", + "code": "no_checkable_claim", + "detail": "No claim in the input could be checked against public evidence.", + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", "failure_class": "invalid_input", "retryable": false, - "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", "docs_url": "https://lenz.io/docs/errors#invalid-input" }, "more_claims": null, @@ -67,6 +68,7 @@ "more_citations": null }, "attempt": 1, - "delivered_at": "2026-10-01T09:42:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__test_fire.json b/tests/fixtures/parity/canonical/webhook__test_fire.json new file mode 100644 index 0000000..1d3d49b --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__test_fire.json @@ -0,0 +1,12 @@ +{ + "status": 200, + "body": { + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00", + "event": "webhook.test", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "status": "test", + "task_id": "test_7cff18da2d979d63" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_completed.json b/tests/fixtures/parity/canonical/webhook__verification_completed.json index 64c7f2c..71884cd 100644 --- a/tests/fixtures/parity/canonical/webhook__verification_completed.json +++ b/tests/fixtures/parity/canonical/webhook__verification_completed.json @@ -1,16 +1,14 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.completed", - "event_id": "evt_0b92b443eb69b9c71434fdc6", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", "verification_id": "68ca4aaf", - "batch_id": null, "status": "completed", "verification": { "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", @@ -37,7 +35,8 @@ "Only two sources were found." ], "suggested_rewrite": "The Eiffel Tower is in Paris.", - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "Example Encyclopedia", @@ -99,11 +98,11 @@ "cap": 10000, "aggregate": 500000, "terms_version": "v1" - }, - "completed_at": "2026-10-01T09:21:00.123456+00:00" + } } }, "attempt": 1, - "delivered_at": "2026-10-01T09:28:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__verification_completed_in_batch.json b/tests/fixtures/parity/canonical/webhook__verification_completed_in_batch.json index f5a306c..507e63b 100644 --- a/tests/fixtures/parity/canonical/webhook__verification_completed_in_batch.json +++ b/tests/fixtures/parity/canonical/webhook__verification_completed_in_batch.json @@ -1,16 +1,15 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.completed", - "event_id": "evt_0b92b443eb69b9c71434fdc6", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", "verification_id": "68ca4aaf", - "batch_id": "4ee0c3aa1b671eaa", + "batch_id": "7ea9cdbd4064e3cf", "status": "completed", "verification": { "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", @@ -37,7 +36,8 @@ "Only two sources were found." ], "suggested_rewrite": "The Eiffel Tower is in Paris.", - "created_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "completed_at": "2026-10-01T09:07:00.123456+00:00", "sources": [ { "source_name": "Example Encyclopedia", @@ -99,11 +99,11 @@ "cap": 10000, "aggregate": 500000, "terms_version": "v1" - }, - "completed_at": "2026-10-01T09:28:00.123456+00:00" + } } }, "attempt": 1, - "delivered_at": "2026-10-01T09:35:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json index 2bbe988..dc19418 100644 --- a/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json @@ -1,16 +1,14 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.completed", - "event_id": "evt_0b92b443eb69b9c71434fdc6", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", "verification_id": "68ca4aaf", - "batch_id": null, "status": "completed", "verification": { "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", @@ -38,6 +36,7 @@ ], "suggested_rewrite": "The Eiffel Tower is in Paris.", "created_at": "2026-10-01T23:55:00.123456+00:00", + "completed_at": "2026-10-02T00:03:00.123456+00:00", "sources": [ { "source_name": "Example Encyclopedia", @@ -99,11 +98,11 @@ "cap": 10000, "aggregate": 500000, "terms_version": "v1" - }, - "completed_at": "2026-10-02T00:03:00.123456+00:00" + } } }, "attempt": 1, - "delivered_at": "2026-10-01T09:35:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_same_day_hours_apart.json index fa61911..189ca65 100644 --- a/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/canonical/webhook__verification_completed_modified_at_same_day_hours_apart.json @@ -1,16 +1,14 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.completed", - "event_id": "evt_0b92b443eb69b9c71434fdc6", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", "verification_id": "68ca4aaf", - "batch_id": null, "status": "completed", "verification": { "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", @@ -38,6 +36,7 @@ ], "suggested_rewrite": "The Eiffel Tower is in Paris.", "created_at": "2026-10-01T08:00:00.123456+00:00", + "completed_at": "2026-10-01T20:00:00.123456+00:00", "sources": [ { "source_name": "Example Encyclopedia", @@ -99,11 +98,11 @@ "cap": 10000, "aggregate": 500000, "terms_version": "v1" - }, - "completed_at": "2026-10-01T20:00:00.123456+00:00" + } } }, "attempt": 1, - "delivered_at": "2026-10-01T09:28:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__verification_completed_sparse_result.json b/tests/fixtures/parity/canonical/webhook__verification_completed_sparse_result.json new file mode 100644 index 0000000..91f7632 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_completed_sparse_result.json @@ -0,0 +1,22 @@ +{ + "status": 200, + "body": { + "event": "verification.completed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", + "status": "completed", + "verification": { + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "language": "en" + } + }, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_failed_error_none.json b/tests/fixtures/parity/canonical/webhook__verification_failed_error_none.json new file mode 100644 index 0000000..e858af3 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_failed_error_none.json @@ -0,0 +1,24 @@ +{ + "status": 200, + "body": { + "event": "verification.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "failed", + "verification": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "failure": { + "code": "", + "detail": "The check stopped before it finished.", + "hint": null, + "failure_class": "internal", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#internal" + } + }, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_failed_insufficient_evidence.json b/tests/fixtures/parity/canonical/webhook__verification_failed_insufficient_evidence.json index f47e7ee..7d3a875 100644 --- a/tests/fixtures/parity/canonical/webhook__verification_failed_insufficient_evidence.json +++ b/tests/fixtures/parity/canonical/webhook__verification_failed_insufficient_evidence.json @@ -1,26 +1,24 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.failed", - "event_id": "evt_7cff18da2d979d6370e54a16", + "event_id": "evt_09301870aba4e8f6ae33ff60", "task_id": "7cff18da2d979d6370e54a160be121ad", - "verification_id": null, - "batch_id": null, "status": "failed", "verification": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", "failure": { "code": "research_insufficient", - "detail": "A sentence on what happened.", + "detail": "Too few sources about the claim were found to judge it.", "hint": null, "failure_class": "insufficient_evidence", "retryable": false, - "docs_url": "https://lenz.io/docs/errors" + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" } }, "attempt": 1, - "delivered_at": "2026-10-01T09:14:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__verification_failed_not_a_claim.json b/tests/fixtures/parity/canonical/webhook__verification_failed_not_a_claim.json index cf284d5..e25ed20 100644 --- a/tests/fixtures/parity/canonical/webhook__verification_failed_not_a_claim.json +++ b/tests/fixtures/parity/canonical/webhook__verification_failed_not_a_claim.json @@ -1,26 +1,24 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.failed", - "event_id": "evt_7cff18da2d979d6370e54a16", + "event_id": "evt_09301870aba4e8f6ae33ff60", "task_id": "7cff18da2d979d6370e54a160be121ad", - "verification_id": null, - "batch_id": null, "status": "failed", "verification": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", "failure": { "code": "no_checkable_claim", - "detail": "A sentence on what happened.", - "hint": null, + "detail": "No claim in the input could be checked against public evidence.", + "hint": "It is a request to write an essay. Send one factual claim, or run the text through /extract to enumerate its claims.", "failure_class": "invalid_input", "retryable": false, - "docs_url": "https://lenz.io/docs/errors" + "docs_url": "https://lenz.io/docs/errors#invalid-input" } }, "attempt": 1, - "delivered_at": "2026-10-01T09:14:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__verification_failed_upstream_unavailable.json b/tests/fixtures/parity/canonical/webhook__verification_failed_upstream_unavailable.json index 322b4ba..926b1c1 100644 --- a/tests/fixtures/parity/canonical/webhook__verification_failed_upstream_unavailable.json +++ b/tests/fixtures/parity/canonical/webhook__verification_failed_upstream_unavailable.json @@ -1,26 +1,24 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.failed", - "event_id": "evt_7cff18da2d979d6370e54a16", + "event_id": "evt_09301870aba4e8f6ae33ff60", "task_id": "7cff18da2d979d6370e54a160be121ad", - "verification_id": null, - "batch_id": null, "status": "failed", "verification": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", "failure": { "code": "framing_failed", - "detail": "A sentence on what happened.", + "detail": "The input could not be read as a claim.", "hint": null, "failure_class": "upstream_unavailable", "retryable": true, - "docs_url": "https://lenz.io/docs/errors" + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" } }, "attempt": 1, - "delivered_at": "2026-10-01T09:14:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/canonical/webhook__verification_needs_input_hint_none.json b/tests/fixtures/parity/canonical/webhook__verification_needs_input_hint_none.json new file mode 100644 index 0000000..3377675 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_needs_input_hint_none.json @@ -0,0 +1,24 @@ +{ + "status": 200, + "body": { + "event": "verification.needs_input", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "needs_input", + "verification": { + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "reason": "multi_claim", + "claims": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "domain": "" + } + ], + "hint": null + }, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_needs_input_multi_claim.json b/tests/fixtures/parity/canonical/webhook__verification_needs_input_multi_claim.json index 0961dbb..3e04f93 100644 --- a/tests/fixtures/parity/canonical/webhook__verification_needs_input_multi_claim.json +++ b/tests/fixtures/parity/canonical/webhook__verification_needs_input_multi_claim.json @@ -1,18 +1,14 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.needs_input", - "event_id": "evt_7cff18da2d979d6370e54a16", + "event_id": "evt_09301870aba4e8f6ae33ff60", "task_id": "7cff18da2d979d6370e54a160be121ad", - "verification_id": null, - "batch_id": null, "status": "needs_input", "verification": { "status": "needs_input", "task_id": "7cff18da2d979d6370e54a160be121ad", "reason": "multi_claim", - "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", "claims": [ { "claim": "The Earth is round.", @@ -22,9 +18,11 @@ "claim": "The Moon is made of cheese.", "domain": "Science" } - ] + ], + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select." }, "attempt": 1, - "delivered_at": "2026-10-01T09:14:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/expected/account__api_root.json b/tests/fixtures/parity/expected/account__api_root.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__api_root.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__ask_history_after_reset.json b/tests/fixtures/parity/expected/account__ask_history_after_reset.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__ask_history_after_reset.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__ask_history_after_send.json b/tests/fixtures/parity/expected/account__ask_history_after_send.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__ask_history_after_send.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__ask_history_empty.json b/tests/fixtures/parity/expected/account__ask_history_empty.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__ask_history_empty.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__ask_history_messages.json b/tests/fixtures/parity/expected/account__ask_history_messages.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__ask_history_messages.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__ask_reset.json b/tests/fixtures/parity/expected/account__ask_reset.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__ask_reset.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__ask_send.json b/tests/fixtures/parity/expected/account__ask_send.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__ask_send.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__ask_send_language.json b/tests/fixtures/parity/expected/account__ask_send_language.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__ask_send_language.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__library_empty.json b/tests/fixtures/parity/expected/account__library_empty.json index d628806..2184439 100644 --- a/tests/fixtures/parity/expected/account__library_empty.json +++ b/tests/fixtures/parity/expected/account__library_empty.json @@ -5,5 +5,12 @@ "page_size": 20, "total": 0 }, - "render_json": null + "dump_unset": { + "items": [], + "page": 1, + "page_size": 20, + "total": 0 + }, + "render_json": null, + "repr": "LibraryList(items=[], total=0, page=1, page_size=20)" } diff --git a/tests/fixtures/parity/expected/account__library_item_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/account__library_item_modified_at_crosses_midnight_by_minutes.json index dedd58d..d6bafee 100644 --- a/tests/fixtures/parity/expected/account__library_item_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/expected/account__library_item_modified_at_crosses_midnight_by_minutes.json @@ -21,5 +21,28 @@ "page_size": 20, "total": 1 }, - "render_json": null + "dump_unset": { + "items": [ + { + "claim": "A library claim.", + "confidence": "low", + "created_at": "2026-10-01T23:55:00.123456+00:00", + "domain": "", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 1, + "page_size": 20, + "total": 1 + }, + "render_json": null, + "repr": "LibraryList(items=[LibraryItem(verification_id='68ca4aaf', claim='A library claim.', domain='', entities=[], verdict='True', confidence='low', lenz_score=10, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T23:55:00.123456+00:00', modified_at='2026-10-02T00:03:00.123456+00:00', language='en')], total=1, page=1, page_size=20)" } diff --git a/tests/fixtures/parity/expected/account__library_item_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/account__library_item_modified_at_same_day_hours_apart.json index 7edcd92..3e2043e 100644 --- a/tests/fixtures/parity/expected/account__library_item_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/expected/account__library_item_modified_at_same_day_hours_apart.json @@ -21,5 +21,28 @@ "page_size": 20, "total": 1 }, - "render_json": null + "dump_unset": { + "items": [ + { + "claim": "A library claim.", + "confidence": "low", + "created_at": "2026-10-01T08:00:00.123456+00:00", + "domain": "", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 1, + "page_size": 20, + "total": 1 + }, + "render_json": null, + "repr": "LibraryList(items=[LibraryItem(verification_id='68ca4aaf', claim='A library claim.', domain='', entities=[], verdict='True', confidence='low', lenz_score=10, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T08:00:00.123456+00:00', modified_at=None, language='en')], total=1, page=1, page_size=20)" } diff --git a/tests/fixtures/parity/expected/account__library_items.json b/tests/fixtures/parity/expected/account__library_items.json index 72a7d37..00d7d2f 100644 --- a/tests/fixtures/parity/expected/account__library_items.json +++ b/tests/fixtures/parity/expected/account__library_items.json @@ -4,7 +4,7 @@ { "claim": "Library claim 0.", "confidence": "low", - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "domain": "Science", "entities": [], "executive_summary": "The claim is verified.", @@ -19,7 +19,22 @@ { "claim": "Library claim 1.", "confidence": "low", - "created_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf" + }, + { + "claim": "Library claim 2.", + "confidence": "low", + "created_at": "2026-10-01T09:21:00.123456+00:00", "domain": "Science", "entities": [], "executive_summary": "The claim is verified.", @@ -30,11 +45,48 @@ "suggested_rewrite": null, "verdict": "True", "verification_id": "7c83247c" + } + ], + "page": 1, + "page_size": 20, + "total": 3 + }, + "dump_unset": { + "items": [ + { + "claim": "Library claim 0.", + "confidence": "low", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + }, + { + "claim": "Library claim 1.", + "confidence": "low", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "4d44dfaf" }, { "claim": "Library claim 2.", "confidence": "low", - "created_at": "2026-10-01T09:42:00.123456+00:00", + "created_at": "2026-10-01T09:21:00.123456+00:00", "domain": "Science", "entities": [], "executive_summary": "The claim is verified.", @@ -44,12 +96,13 @@ "modified_at": null, "suggested_rewrite": null, "verdict": "True", - "verification_id": "b79019e4" + "verification_id": "7c83247c" } ], "page": 1, "page_size": 20, "total": 3 }, - "render_json": null + "render_json": null, + "repr": "LibraryList(items=[LibraryItem(verification_id='68ca4aaf', claim='Library claim 0.', domain='Science', entities=[], verdict='True', confidence='low', lenz_score=10, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en'), LibraryItem(verification_id='4d44dfaf', claim='Library claim 1.', domain='Science', entities=[], verdict='True', confidence='low', lenz_score=10, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T09:14:00.123456+00:00', modified_at=None, language='en'), LibraryItem(verification_id='7c83247c', claim='Library claim 2.', domain='Science', entities=[], verdict='True', confidence='low', lenz_score=10, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T09:21:00.123456+00:00', modified_at=None, language='en')], total=3, page=1, page_size=20)" } diff --git a/tests/fixtures/parity/expected/account__library_no_match.json b/tests/fixtures/parity/expected/account__library_no_match.json index d628806..2184439 100644 --- a/tests/fixtures/parity/expected/account__library_no_match.json +++ b/tests/fixtures/parity/expected/account__library_no_match.json @@ -5,5 +5,12 @@ "page_size": 20, "total": 0 }, - "render_json": null + "dump_unset": { + "items": [], + "page": 1, + "page_size": 20, + "total": 0 + }, + "render_json": null, + "repr": "LibraryList(items=[], total=0, page=1, page_size=20)" } diff --git a/tests/fixtures/parity/expected/account__library_page_past_end.json b/tests/fixtures/parity/expected/account__library_page_past_end.json index 437be04..fc8bd17 100644 --- a/tests/fixtures/parity/expected/account__library_page_past_end.json +++ b/tests/fixtures/parity/expected/account__library_page_past_end.json @@ -5,5 +5,12 @@ "page_size": 20, "total": 1 }, - "render_json": null + "dump_unset": { + "items": [], + "page": 2, + "page_size": 20, + "total": 1 + }, + "render_json": null, + "repr": "LibraryList(items=[], total=1, page=2, page_size=20)" } diff --git a/tests/fixtures/parity/expected/account__library_paginated_second_page.json b/tests/fixtures/parity/expected/account__library_paginated_second_page.json index 6d4d3d3..e572a47 100644 --- a/tests/fixtures/parity/expected/account__library_paginated_second_page.json +++ b/tests/fixtures/parity/expected/account__library_paginated_second_page.json @@ -4,6 +4,21 @@ { "claim": "Paged claim 020.", "confidence": "low", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "domain": "", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + }, + { + "claim": "Paged claim 021.", + "confidence": "low", "created_at": "2026-10-01T09:14:00.123456+00:00", "domain": "", "entities": [], @@ -14,12 +29,34 @@ "modified_at": null, "suggested_rewrite": null, "verdict": "True", + "verification_id": "4d44dfaf" + } + ], + "page": 2, + "page_size": 20, + "total": 22 + }, + "dump_unset": { + "items": [ + { + "claim": "Paged claim 020.", + "confidence": "low", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "domain": "", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 10, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", "verification_id": "68ca4aaf" }, { "claim": "Paged claim 021.", "confidence": "low", - "created_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "domain": "", "entities": [], "executive_summary": "The claim is verified.", @@ -29,12 +66,13 @@ "modified_at": null, "suggested_rewrite": null, "verdict": "True", - "verification_id": "7c83247c" + "verification_id": "4d44dfaf" } ], "page": 2, "page_size": 20, "total": 22 }, - "render_json": null + "render_json": null, + "repr": "LibraryList(items=[LibraryItem(verification_id='68ca4aaf', claim='Paged claim 020.', domain='', entities=[], verdict='True', confidence='low', lenz_score=10, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en'), LibraryItem(verification_id='4d44dfaf', claim='Paged claim 021.', domain='', entities=[], verdict='True', confidence='low', lenz_score=10, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T09:14:00.123456+00:00', modified_at=None, language='en')], total=22, page=2, page_size=20)" } diff --git a/tests/fixtures/parity/expected/account__me_usage_extra_credits.json b/tests/fixtures/parity/expected/account__me_usage_extra_credits.json index 4b02079..3099e7d 100644 --- a/tests/fixtures/parity/expected/account__me_usage_extra_credits.json +++ b/tests/fixtures/parity/expected/account__me_usage_extra_credits.json @@ -35,7 +35,7 @@ "bonus": 250, "extra": 250, "remaining": 350, - "resets_at": "2026-10-01T09:07:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "total": 350, "used": 0 }, @@ -47,7 +47,65 @@ "has_webhook_secret": false, "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", + "verify": { + "bonus": 25, + "credits": 25, + "quota_remaining": 35, + "quota_total": 35, + "quota_used": 0, + "remaining": 35 + } + }, + "dump_unset": { + "ask": { + "bonus": 250, + "credits": 250, + "quota_remaining": 350, + "quota_total": 350, + "quota_used": 0, + "remaining": 350 + }, + "assess": { + "bonus": 250, + "credits": 250, + "quota_remaining": 350, + "quota_total": 350, + "quota_used": 0, + "remaining": 350 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 250, + "extra": 250, + "remaining": 350, + "resets_at": "2026-10-01T14:07:00+00:00", + "total": 350, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "verify": { "bonus": 25, "credits": 25, @@ -58,5 +116,6 @@ } }, "render": "Lenz usage (Free plan)\n 350 credits left (≈ 35 verifications · 350 assessments)\n Verify: 35 left (0 / 35 quota + 25 extra · 10 credits each · 5 at depth \"low\")\n Ask: 350 left (0 / 350 quota + 250 extra · 1 credit each)\n Assess: 350 left (0 / 350 quota + 250 extra · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", - "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 350,\n \"used\": 0,\n \"remaining\": 350,\n \"extra\": 250,\n \"bonus\": 250,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 35,\n \"quota_remaining\": 35,\n \"bonus\": 25,\n \"credits\": 25,\n \"remaining\": 35\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 350,\n \"quota_remaining\": 350,\n \"bonus\": 250,\n \"credits\": 250,\n \"remaining\": 350\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 350,\n \"quota_remaining\": 350,\n \"bonus\": 250,\n \"credits\": 250,\n \"remaining\": 350\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T14:07:00+00:00\",\n \"credits\": {\n \"total\": 350,\n \"used\": 0,\n \"remaining\": 350,\n \"extra\": 250,\n \"bonus\": 250,\n \"resets_at\": \"2026-10-01T14:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 35,\n \"quota_remaining\": 35,\n \"bonus\": 25,\n \"credits\": 25,\n \"remaining\": 35\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 350,\n \"quota_remaining\": 350,\n \"bonus\": 250,\n \"credits\": 250,\n \"remaining\": 350\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 350,\n \"quota_remaining\": 350,\n \"bonus\": 250,\n \"credits\": 250,\n \"remaining\": 350\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='free', plan_label='Free', quota_resets_at='2026-10-01T14:07:00+00:00', credits=UsageCredits(total=350, used=0, remaining=350, extra=250, bonus=250, resets_at='2026-10-01T14:07:00+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=0, quota_total=35, quota_remaining=35, bonus=25, credits=25, remaining=35), ask=UsageCapacity(quota_used=0, quota_total=350, quota_remaining=350, bonus=250, credits=250, remaining=350), assess=UsageCapacity(quota_used=0, quota_total=350, quota_remaining=350, bonus=250, credits=250, remaining=350), extract=UsageExtract(calls_today=0, daily_limit=1000, unlimited=False), has_webhook_secret=False)" } diff --git a/tests/fixtures/parity/expected/account__me_usage_extra_only.json b/tests/fixtures/parity/expected/account__me_usage_extra_only.json index fbd9a1f..e1f90b6 100644 --- a/tests/fixtures/parity/expected/account__me_usage_extra_only.json +++ b/tests/fixtures/parity/expected/account__me_usage_extra_only.json @@ -35,7 +35,7 @@ "bonus": 37, "extra": 37, "remaining": 37, - "resets_at": "2026-10-01T09:07:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "total": 137, "used": 100 }, @@ -47,7 +47,65 @@ "has_webhook_secret": false, "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", + "verify": { + "bonus": 3, + "credits": 3, + "quota_remaining": 3, + "quota_total": 13, + "quota_used": 10, + "remaining": 3 + } + }, + "dump_unset": { + "ask": { + "bonus": 37, + "credits": 37, + "quota_remaining": 37, + "quota_total": 137, + "quota_used": 100, + "remaining": 37 + }, + "assess": { + "bonus": 37, + "credits": 37, + "quota_remaining": 37, + "quota_total": 137, + "quota_used": 100, + "remaining": 37 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 37, + "extra": 37, + "remaining": 37, + "resets_at": "2026-10-01T14:07:00+00:00", + "total": 137, + "used": 100 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "verify": { "bonus": 3, "credits": 3, @@ -58,5 +116,6 @@ } }, "render": "Lenz usage (Free plan)\n 37 credits left (≈ 3 verifications · 37 assessments)\n Verify: 3 left (10 / 13 quota + 3 extra · 10 credits each · 5 at depth \"low\")\n Ask: 37 left (100 / 137 quota + 37 extra · 1 credit each)\n Assess: 37 left (100 / 137 quota + 37 extra · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", - "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 137,\n \"used\": 100,\n \"remaining\": 37,\n \"extra\": 37,\n \"bonus\": 37,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 10,\n \"quota_total\": 13,\n \"quota_remaining\": 3,\n \"bonus\": 3,\n \"credits\": 3,\n \"remaining\": 3\n },\n \"ask\": {\n \"quota_used\": 100,\n \"quota_total\": 137,\n \"quota_remaining\": 37,\n \"bonus\": 37,\n \"credits\": 37,\n \"remaining\": 37\n },\n \"assess\": {\n \"quota_used\": 100,\n \"quota_total\": 137,\n \"quota_remaining\": 37,\n \"bonus\": 37,\n \"credits\": 37,\n \"remaining\": 37\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T14:07:00+00:00\",\n \"credits\": {\n \"total\": 137,\n \"used\": 100,\n \"remaining\": 37,\n \"extra\": 37,\n \"bonus\": 37,\n \"resets_at\": \"2026-10-01T14:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 10,\n \"quota_total\": 13,\n \"quota_remaining\": 3,\n \"bonus\": 3,\n \"credits\": 3,\n \"remaining\": 3\n },\n \"ask\": {\n \"quota_used\": 100,\n \"quota_total\": 137,\n \"quota_remaining\": 37,\n \"bonus\": 37,\n \"credits\": 37,\n \"remaining\": 37\n },\n \"assess\": {\n \"quota_used\": 100,\n \"quota_total\": 137,\n \"quota_remaining\": 37,\n \"bonus\": 37,\n \"credits\": 37,\n \"remaining\": 37\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='free', plan_label='Free', quota_resets_at='2026-10-01T14:07:00+00:00', credits=UsageCredits(total=137, used=100, remaining=37, extra=37, bonus=37, resets_at='2026-10-01T14:07:00+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=10, quota_total=13, quota_remaining=3, bonus=3, credits=3, remaining=3), ask=UsageCapacity(quota_used=100, quota_total=137, quota_remaining=37, bonus=37, credits=37, remaining=37), assess=UsageCapacity(quota_used=100, quota_total=137, quota_remaining=37, bonus=37, credits=37, remaining=37), extract=UsageExtract(calls_today=0, daily_limit=1000, unlimited=False), has_webhook_secret=False)" } diff --git a/tests/fixtures/parity/expected/account__me_usage_extract_calls.json b/tests/fixtures/parity/expected/account__me_usage_extract_calls.json index cfb8de5..5cfc97c 100644 --- a/tests/fixtures/parity/expected/account__me_usage_extract_calls.json +++ b/tests/fixtures/parity/expected/account__me_usage_extract_calls.json @@ -35,7 +35,7 @@ "bonus": 0, "extra": 0, "remaining": 100, - "resets_at": "2026-10-01T09:07:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "total": 100, "used": 0 }, @@ -47,7 +47,65 @@ "has_webhook_secret": false, "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 10, + "quota_total": 10, + "quota_used": 0, + "remaining": 10 + } + }, + "dump_unset": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 100, + "resets_at": "2026-10-01T14:07:00+00:00", + "total": 100, + "used": 0 + }, + "extract": { + "calls_today": 7, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "verify": { "bonus": 0, "credits": 0, @@ -58,5 +116,6 @@ } }, "render": "Lenz usage (Free plan)\n 100 credits left (≈ 10 verifications · 100 assessments)\n Verify: 10 left (0 / 10 quota · 10 credits each · 5 at depth \"low\")\n Ask: 100 left (0 / 100 quota · 1 credit each)\n Assess: 100 left (0 / 100 quota · 1 credit each)\n Extract: 7 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", - "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 7,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T14:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T14:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 7,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='free', plan_label='Free', quota_resets_at='2026-10-01T14:07:00+00:00', credits=UsageCredits(total=100, used=0, remaining=100, extra=0, bonus=0, resets_at='2026-10-01T14:07:00+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=0, quota_total=10, quota_remaining=10, bonus=0, credits=0, remaining=10), ask=UsageCapacity(quota_used=0, quota_total=100, quota_remaining=100, bonus=0, credits=0, remaining=100), assess=UsageCapacity(quota_used=0, quota_total=100, quota_remaining=100, bonus=0, credits=0, remaining=100), extract=UsageExtract(calls_today=7, daily_limit=1000, unlimited=False), has_webhook_secret=False)" } diff --git a/tests/fixtures/parity/expected/account__me_usage_free.json b/tests/fixtures/parity/expected/account__me_usage_free.json index 5d7b08d..f2f3166 100644 --- a/tests/fixtures/parity/expected/account__me_usage_free.json +++ b/tests/fixtures/parity/expected/account__me_usage_free.json @@ -35,7 +35,7 @@ "bonus": 0, "extra": 0, "remaining": 100, - "resets_at": "2026-10-01T09:07:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "total": 100, "used": 0 }, @@ -47,7 +47,65 @@ "has_webhook_secret": false, "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 10, + "quota_total": 10, + "quota_used": 0, + "remaining": 10 + } + }, + "dump_unset": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 100, + "resets_at": "2026-10-01T14:07:00+00:00", + "total": 100, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "verify": { "bonus": 0, "credits": 0, @@ -58,5 +116,6 @@ } }, "render": "Lenz usage (Free plan)\n 100 credits left (≈ 10 verifications · 100 assessments)\n Verify: 10 left (0 / 10 quota · 10 credits each · 5 at depth \"low\")\n Ask: 100 left (0 / 100 quota · 1 credit each)\n Assess: 100 left (0 / 100 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", - "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T14:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T14:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='free', plan_label='Free', quota_resets_at='2026-10-01T14:07:00+00:00', credits=UsageCredits(total=100, used=0, remaining=100, extra=0, bonus=0, resets_at='2026-10-01T14:07:00+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=0, quota_total=10, quota_remaining=10, bonus=0, credits=0, remaining=10), ask=UsageCapacity(quota_used=0, quota_total=100, quota_remaining=100, bonus=0, credits=0, remaining=100), assess=UsageCapacity(quota_used=0, quota_total=100, quota_remaining=100, bonus=0, credits=0, remaining=100), extract=UsageExtract(calls_today=0, daily_limit=1000, unlimited=False), has_webhook_secret=False)" } diff --git a/tests/fixtures/parity/expected/account__me_usage_free_partly_spent.json b/tests/fixtures/parity/expected/account__me_usage_free_partly_spent.json index 61b0e4c..d353d1e 100644 --- a/tests/fixtures/parity/expected/account__me_usage_free_partly_spent.json +++ b/tests/fixtures/parity/expected/account__me_usage_free_partly_spent.json @@ -35,7 +35,7 @@ "bonus": 0, "extra": 0, "remaining": 83, - "resets_at": "2026-10-01T09:07:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "total": 100, "used": 17 }, @@ -47,7 +47,65 @@ "has_webhook_secret": false, "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 8, + "quota_total": 10, + "quota_used": 2, + "remaining": 8 + } + }, + "dump_unset": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 83, + "quota_total": 100, + "quota_used": 17, + "remaining": 83 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 83, + "quota_total": 100, + "quota_used": 17, + "remaining": 83 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 83, + "resets_at": "2026-10-01T14:07:00+00:00", + "total": 100, + "used": 17 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "verify": { "bonus": 0, "credits": 0, @@ -58,5 +116,6 @@ } }, "render": "Lenz usage (Free plan)\n 83 credits left (≈ 8 verifications · 83 assessments)\n Verify: 8 left (2 / 10 quota · 10 credits each · 5 at depth \"low\")\n Ask: 83 left (17 / 100 quota · 1 credit each)\n Assess: 83 left (17 / 100 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", - "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 17,\n \"remaining\": 83,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 2,\n \"quota_total\": 10,\n \"quota_remaining\": 8,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 8\n },\n \"ask\": {\n \"quota_used\": 17,\n \"quota_total\": 100,\n \"quota_remaining\": 83,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 83\n },\n \"assess\": {\n \"quota_used\": 17,\n \"quota_total\": 100,\n \"quota_remaining\": 83,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 83\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T14:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 17,\n \"remaining\": 83,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T14:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 2,\n \"quota_total\": 10,\n \"quota_remaining\": 8,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 8\n },\n \"ask\": {\n \"quota_used\": 17,\n \"quota_total\": 100,\n \"quota_remaining\": 83,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 83\n },\n \"assess\": {\n \"quota_used\": 17,\n \"quota_total\": 100,\n \"quota_remaining\": 83,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 83\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='free', plan_label='Free', quota_resets_at='2026-10-01T14:07:00+00:00', credits=UsageCredits(total=100, used=17, remaining=83, extra=0, bonus=0, resets_at='2026-10-01T14:07:00+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=2, quota_total=10, quota_remaining=8, bonus=0, credits=0, remaining=8), ask=UsageCapacity(quota_used=17, quota_total=100, quota_remaining=83, bonus=0, credits=0, remaining=83), assess=UsageCapacity(quota_used=17, quota_total=100, quota_remaining=83, bonus=0, credits=0, remaining=83), extract=UsageExtract(calls_today=0, daily_limit=1000, unlimited=False), has_webhook_secret=False)" } diff --git a/tests/fixtures/parity/expected/account__me_usage_oauth.json b/tests/fixtures/parity/expected/account__me_usage_oauth.json new file mode 100644 index 0000000..f2f3166 --- /dev/null +++ b/tests/fixtures/parity/expected/account__me_usage_oauth.json @@ -0,0 +1,121 @@ +{ + "dump": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 100, + "resets_at": "2026-10-01T14:07:00+00:00", + "total": 100, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T14:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 10, + "quota_total": 10, + "quota_used": 0, + "remaining": 10 + } + }, + "dump_unset": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 100, + "resets_at": "2026-10-01T14:07:00+00:00", + "total": 100, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T14:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 10, + "quota_total": 10, + "quota_used": 0, + "remaining": 10 + } + }, + "render": "Lenz usage (Free plan)\n 100 credits left (≈ 10 verifications · 100 assessments)\n Verify: 10 left (0 / 10 quota · 10 credits each · 5 at depth \"low\")\n Ask: 100 left (0 / 100 quota · 1 credit each)\n Assess: 100 left (0 / 100 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T14:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T14:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='free', plan_label='Free', quota_resets_at='2026-10-01T14:07:00+00:00', credits=UsageCredits(total=100, used=0, remaining=100, extra=0, bonus=0, resets_at='2026-10-01T14:07:00+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=0, quota_total=10, quota_remaining=10, bonus=0, credits=0, remaining=10), ask=UsageCapacity(quota_used=0, quota_total=100, quota_remaining=100, bonus=0, credits=0, remaining=100), assess=UsageCapacity(quota_used=0, quota_total=100, quota_remaining=100, bonus=0, credits=0, remaining=100), extract=UsageExtract(calls_today=0, daily_limit=1000, unlimited=False), has_webhook_secret=False)" +} diff --git a/tests/fixtures/parity/expected/account__me_usage_plus.json b/tests/fixtures/parity/expected/account__me_usage_plus.json index 240dcb2..faadaf1 100644 --- a/tests/fixtures/parity/expected/account__me_usage_plus.json +++ b/tests/fixtures/parity/expected/account__me_usage_plus.json @@ -35,7 +35,7 @@ "bonus": 0, "extra": 0, "remaining": 500, - "resets_at": "2026-10-01T09:07:00.123456+00:00", + "resets_at": "2026-10-01T14:07:00.123456+00:00", "total": 500, "used": 0 }, @@ -47,7 +47,65 @@ "has_webhook_secret": false, "plan": "plus", "plan_label": "Plus", - "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "quota_resets_at": "2026-10-01T14:07:00.123456+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 50, + "quota_total": 50, + "quota_used": 0, + "remaining": 50 + } + }, + "dump_unset": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 500, + "quota_total": 500, + "quota_used": 0, + "remaining": 500 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 500, + "quota_total": 500, + "quota_used": 0, + "remaining": 500 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 500, + "resets_at": "2026-10-01T14:07:00.123456+00:00", + "total": 500, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "plus", + "plan_label": "Plus", + "quota_resets_at": "2026-10-01T14:07:00.123456+00:00", "verify": { "bonus": 0, "credits": 0, @@ -58,5 +116,6 @@ } }, "render": "Lenz usage (Plus plan)\n 500 credits left (≈ 50 verifications · 500 assessments)\n Verify: 50 left (0 / 50 quota · 10 credits each · 5 at depth \"low\")\n Ask: 500 left (0 / 500 quota · 1 credit each)\n Assess: 500 left (0 / 500 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", - "render_json": "{\n \"plan\": \"plus\",\n \"plan_label\": \"Plus\",\n \"quota_resets_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"credits\": {\n \"total\": 500,\n \"used\": 0,\n \"remaining\": 500,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00.123456+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 50,\n \"quota_remaining\": 50,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 50\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 500,\n \"quota_remaining\": 500,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 500\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 500,\n \"quota_remaining\": 500,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 500\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" + "render_json": "{\n \"plan\": \"plus\",\n \"plan_label\": \"Plus\",\n \"quota_resets_at\": \"2026-10-01T14:07:00.123456+00:00\",\n \"credits\": {\n \"total\": 500,\n \"used\": 0,\n \"remaining\": 500,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T14:07:00.123456+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 50,\n \"quota_remaining\": 50,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 50\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 500,\n \"quota_remaining\": 500,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 500\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 500,\n \"quota_remaining\": 500,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 500\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='plus', plan_label='Plus', quota_resets_at='2026-10-01T14:07:00.123456+00:00', credits=UsageCredits(total=500, used=0, remaining=500, extra=0, bonus=0, resets_at='2026-10-01T14:07:00.123456+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=0, quota_total=50, quota_remaining=50, bonus=0, credits=0, remaining=50), ask=UsageCapacity(quota_used=0, quota_total=500, quota_remaining=500, bonus=0, credits=0, remaining=500), assess=UsageCapacity(quota_used=0, quota_total=500, quota_remaining=500, bonus=0, credits=0, remaining=500), extract=UsageExtract(calls_today=0, daily_limit=1000, unlimited=False), has_webhook_secret=False)" } diff --git a/tests/fixtures/parity/expected/account__me_usage_pro.json b/tests/fixtures/parity/expected/account__me_usage_pro.json index 6bd10f7..17433cd 100644 --- a/tests/fixtures/parity/expected/account__me_usage_pro.json +++ b/tests/fixtures/parity/expected/account__me_usage_pro.json @@ -35,7 +35,7 @@ "bonus": 0, "extra": 0, "remaining": 5000, - "resets_at": "2026-10-01T09:07:00.123456+00:00", + "resets_at": "2026-10-01T14:07:00.123456+00:00", "total": 5000, "used": 0 }, @@ -47,7 +47,65 @@ "has_webhook_secret": false, "plan": "pro", "plan_label": "Pro", - "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "quota_resets_at": "2026-10-01T14:07:00.123456+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 500, + "quota_total": 500, + "quota_used": 0, + "remaining": 500 + } + }, + "dump_unset": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 5000, + "quota_total": 5000, + "quota_used": 0, + "remaining": 5000 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 5000, + "quota_total": 5000, + "quota_used": 0, + "remaining": 5000 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 5000, + "resets_at": "2026-10-01T14:07:00.123456+00:00", + "total": 5000, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "pro", + "plan_label": "Pro", + "quota_resets_at": "2026-10-01T14:07:00.123456+00:00", "verify": { "bonus": 0, "credits": 0, @@ -58,5 +116,6 @@ } }, "render": "Lenz usage (Pro plan)\n 5000 credits left (≈ 500 verifications · 5000 assessments)\n Verify: 500 left (0 / 500 quota · 10 credits each · 5 at depth \"low\")\n Ask: 5000 left (0 / 5000 quota · 1 credit each)\n Assess: 5000 left (0 / 5000 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", - "render_json": "{\n \"plan\": \"pro\",\n \"plan_label\": \"Pro\",\n \"quota_resets_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"credits\": {\n \"total\": 5000,\n \"used\": 0,\n \"remaining\": 5000,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00.123456+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 500,\n \"quota_remaining\": 500,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 500\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 5000,\n \"quota_remaining\": 5000,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 5000\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 5000,\n \"quota_remaining\": 5000,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 5000\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" + "render_json": "{\n \"plan\": \"pro\",\n \"plan_label\": \"Pro\",\n \"quota_resets_at\": \"2026-10-01T14:07:00.123456+00:00\",\n \"credits\": {\n \"total\": 5000,\n \"used\": 0,\n \"remaining\": 5000,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T14:07:00.123456+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 500,\n \"quota_remaining\": 500,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 500\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 5000,\n \"quota_remaining\": 5000,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 5000\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 5000,\n \"quota_remaining\": 5000,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 5000\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='pro', plan_label='Pro', quota_resets_at='2026-10-01T14:07:00.123456+00:00', credits=UsageCredits(total=5000, used=0, remaining=5000, extra=0, bonus=0, resets_at='2026-10-01T14:07:00.123456+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=0, quota_total=500, quota_remaining=500, bonus=0, credits=0, remaining=500), ask=UsageCapacity(quota_used=0, quota_total=5000, quota_remaining=5000, bonus=0, credits=0, remaining=5000), assess=UsageCapacity(quota_used=0, quota_total=5000, quota_remaining=5000, bonus=0, credits=0, remaining=5000), extract=UsageExtract(calls_today=0, daily_limit=1000, unlimited=False), has_webhook_secret=False)" } diff --git a/tests/fixtures/parity/expected/account__me_usage_pro_extra.json b/tests/fixtures/parity/expected/account__me_usage_pro_extra.json index ca22ae7..99126d3 100644 --- a/tests/fixtures/parity/expected/account__me_usage_pro_extra.json +++ b/tests/fixtures/parity/expected/account__me_usage_pro_extra.json @@ -35,7 +35,7 @@ "bonus": 1000, "extra": 1000, "remaining": 6000, - "resets_at": "2026-10-01T09:07:00.123456+00:00", + "resets_at": "2026-10-01T14:07:00.123456+00:00", "total": 6000, "used": 0 }, @@ -47,7 +47,65 @@ "has_webhook_secret": false, "plan": "pro", "plan_label": "Pro", - "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "quota_resets_at": "2026-10-01T14:07:00.123456+00:00", + "verify": { + "bonus": 100, + "credits": 100, + "quota_remaining": 600, + "quota_total": 600, + "quota_used": 0, + "remaining": 600 + } + }, + "dump_unset": { + "ask": { + "bonus": 1000, + "credits": 1000, + "quota_remaining": 6000, + "quota_total": 6000, + "quota_used": 0, + "remaining": 6000 + }, + "assess": { + "bonus": 1000, + "credits": 1000, + "quota_remaining": 6000, + "quota_total": 6000, + "quota_used": 0, + "remaining": 6000 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 1000, + "extra": 1000, + "remaining": 6000, + "resets_at": "2026-10-01T14:07:00.123456+00:00", + "total": 6000, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "pro", + "plan_label": "Pro", + "quota_resets_at": "2026-10-01T14:07:00.123456+00:00", "verify": { "bonus": 100, "credits": 100, @@ -58,5 +116,6 @@ } }, "render": "Lenz usage (Pro plan)\n 6000 credits left (≈ 600 verifications · 6000 assessments)\n Verify: 600 left (0 / 600 quota + 100 extra · 10 credits each · 5 at depth \"low\")\n Ask: 6000 left (0 / 6000 quota + 1000 extra · 1 credit each)\n Assess: 6000 left (0 / 6000 quota + 1000 extra · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", - "render_json": "{\n \"plan\": \"pro\",\n \"plan_label\": \"Pro\",\n \"quota_resets_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"credits\": {\n \"total\": 6000,\n \"used\": 0,\n \"remaining\": 6000,\n \"extra\": 1000,\n \"bonus\": 1000,\n \"resets_at\": \"2026-10-01T09:07:00.123456+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 600,\n \"quota_remaining\": 600,\n \"bonus\": 100,\n \"credits\": 100,\n \"remaining\": 600\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 6000,\n \"quota_remaining\": 6000,\n \"bonus\": 1000,\n \"credits\": 1000,\n \"remaining\": 6000\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 6000,\n \"quota_remaining\": 6000,\n \"bonus\": 1000,\n \"credits\": 1000,\n \"remaining\": 6000\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" + "render_json": "{\n \"plan\": \"pro\",\n \"plan_label\": \"Pro\",\n \"quota_resets_at\": \"2026-10-01T14:07:00.123456+00:00\",\n \"credits\": {\n \"total\": 6000,\n \"used\": 0,\n \"remaining\": 6000,\n \"extra\": 1000,\n \"bonus\": 1000,\n \"resets_at\": \"2026-10-01T14:07:00.123456+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 600,\n \"quota_remaining\": 600,\n \"bonus\": 100,\n \"credits\": 100,\n \"remaining\": 600\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 6000,\n \"quota_remaining\": 6000,\n \"bonus\": 1000,\n \"credits\": 1000,\n \"remaining\": 6000\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 6000,\n \"quota_remaining\": 6000,\n \"bonus\": 1000,\n \"credits\": 1000,\n \"remaining\": 6000\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='pro', plan_label='Pro', quota_resets_at='2026-10-01T14:07:00.123456+00:00', credits=UsageCredits(total=6000, used=0, remaining=6000, extra=1000, bonus=1000, resets_at='2026-10-01T14:07:00.123456+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=0, quota_total=600, quota_remaining=600, bonus=100, credits=100, remaining=600), ask=UsageCapacity(quota_used=0, quota_total=6000, quota_remaining=6000, bonus=1000, credits=1000, remaining=6000), assess=UsageCapacity(quota_used=0, quota_total=6000, quota_remaining=6000, bonus=1000, credits=1000, remaining=6000), extract=UsageExtract(calls_today=0, daily_limit=1000, unlimited=False), has_webhook_secret=False)" } diff --git a/tests/fixtures/parity/expected/account__me_usage_stale_quota_row.json b/tests/fixtures/parity/expected/account__me_usage_stale_quota_row.json index 5d7b08d..f2f3166 100644 --- a/tests/fixtures/parity/expected/account__me_usage_stale_quota_row.json +++ b/tests/fixtures/parity/expected/account__me_usage_stale_quota_row.json @@ -35,7 +35,7 @@ "bonus": 0, "extra": 0, "remaining": 100, - "resets_at": "2026-10-01T09:07:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "total": 100, "used": 0 }, @@ -47,7 +47,65 @@ "has_webhook_secret": false, "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", + "verify": { + "bonus": 0, + "credits": 0, + "quota_remaining": 10, + "quota_total": 10, + "quota_used": 0, + "remaining": 10 + } + }, + "dump_unset": { + "ask": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "assess": { + "bonus": 0, + "credits": 0, + "quota_remaining": 100, + "quota_total": 100, + "quota_used": 0, + "remaining": 100 + }, + "cost_options": { + "verify": { + "depth": { + "low": 5, + "standard": 10 + } + } + }, + "costs": { + "ask": 1, + "assess": 1, + "citation": 1, + "extract": 0, + "verify": 10 + }, + "credits": { + "bonus": 0, + "extra": 0, + "remaining": 100, + "resets_at": "2026-10-01T14:07:00+00:00", + "total": 100, + "used": 0 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false, + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "verify": { "bonus": 0, "credits": 0, @@ -58,5 +116,6 @@ } }, "render": "Lenz usage (Free plan)\n 100 credits left (≈ 10 verifications · 100 assessments)\n Verify: 10 left (0 / 10 quota · 10 credits each · 5 at depth \"low\")\n Ask: 100 left (0 / 100 quota · 1 credit each)\n Assess: 100 left (0 / 100 quota · 1 credit each)\n Extract: 0 / 1000 today (free — no credit charge)\n Credits reset Oct 1, 2026\n", - "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T09:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T09:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n" + "render_json": "{\n \"plan\": \"free\",\n \"plan_label\": \"Free\",\n \"quota_resets_at\": \"2026-10-01T14:07:00+00:00\",\n \"credits\": {\n \"total\": 100,\n \"used\": 0,\n \"remaining\": 100,\n \"extra\": 0,\n \"bonus\": 0,\n \"resets_at\": \"2026-10-01T14:07:00+00:00\"\n },\n \"costs\": {\n \"verify\": 10,\n \"assess\": 1,\n \"ask\": 1,\n \"citation\": 1,\n \"extract\": 0\n },\n \"cost_options\": {\n \"verify\": {\n \"depth\": {\n \"standard\": 10,\n \"low\": 5\n }\n }\n },\n \"verify\": {\n \"quota_used\": 0,\n \"quota_total\": 10,\n \"quota_remaining\": 10,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 10\n },\n \"ask\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"assess\": {\n \"quota_used\": 0,\n \"quota_total\": 100,\n \"quota_remaining\": 100,\n \"bonus\": 0,\n \"credits\": 0,\n \"remaining\": 100\n },\n \"extract\": {\n \"calls_today\": 0,\n \"daily_limit\": 1000,\n \"unlimited\": false\n },\n \"has_webhook_secret\": false\n}\n", + "repr": "Usage(plan='free', plan_label='Free', quota_resets_at='2026-10-01T14:07:00+00:00', credits=UsageCredits(total=100, used=0, remaining=100, extra=0, bonus=0, resets_at='2026-10-01T14:07:00+00:00'), costs={'verify': 10, 'assess': 1, 'ask': 1, 'citation': 1, 'extract': 0}, cost_options={'verify': {'depth': {'standard': 10, 'low': 5}}}, verify=UsageCapacity(quota_used=0, quota_total=10, quota_remaining=10, bonus=0, credits=0, remaining=10), ask=UsageCapacity(quota_used=0, quota_total=100, quota_remaining=100, bonus=0, credits=0, remaining=100), assess=UsageCapacity(quota_used=0, quota_total=100, quota_remaining=100, bonus=0, credits=0, remaining=100), extract=UsageExtract(calls_today=0, daily_limit=1000, unlimited=False), has_webhook_secret=False)" } diff --git a/tests/fixtures/parity/expected/account__oauth_register_success.json b/tests/fixtures/parity/expected/account__oauth_register_success.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__oauth_register_success.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__oauth_revoke.json b/tests/fixtures/parity/expected/account__oauth_revoke.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__oauth_revoke.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__oauth_revoke_token.json b/tests/fixtures/parity/expected/account__oauth_revoke_token.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__oauth_revoke_token.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/account__webhook_secret_api_key.json b/tests/fixtures/parity/expected/account__webhook_secret_api_key.json new file mode 100644 index 0000000..99f0634 --- /dev/null +++ b/tests/fixtures/parity/expected/account__webhook_secret_api_key.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "code": "oauth_only", + "doc_url": "https://lenz.io/docs/auth", + "fix": "This key doesn't have access to that resource.", + "friendly_text": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.\n Fix: This key doesn't have access to that resource.", + "message": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "This key doesn't have access to that resource.", + "message": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "status": 403 + } + }, + "request_id": "", + "status_code": 403, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/account__webhook_secret_insufficient_scope.json b/tests/fixtures/parity/expected/account__webhook_secret_insufficient_scope.json new file mode 100644 index 0000000..567f3ae --- /dev/null +++ b/tests/fixtures/parity/expected/account__webhook_secret_insufficient_scope.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "The access token does not allow this call.", + "code": "insufficient_scope", + "doc_url": "https://lenz.io/docs/auth", + "fix": "This key doesn't have access to that resource.", + "friendly_text": "The access token does not allow this call.\n Fix: This key doesn't have access to that resource.", + "message": "The access token does not allow this call.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "This key doesn't have access to that resource.", + "message": "The access token does not allow this call.", + "status": 403 + } + }, + "request_id": "", + "status_code": 403, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/account__webhook_secret_oauth.json b/tests/fixtures/parity/expected/account__webhook_secret_oauth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__webhook_secret_oauth.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/assess__402_no_credits.json b/tests/fixtures/parity/expected/assess__402_no_credits.json index 732bc39..91af7cd 100644 --- a/tests/fixtures/parity/expected/assess__402_no_credits.json +++ b/tests/fixtures/parity/expected/assess__402_no_credits.json @@ -20,7 +20,7 @@ "remaining": 0, "request_id": "", "requested": null, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/assess__deep_tier_hit.json b/tests/fixtures/parity/expected/assess__deep_tier_hit.json index 89424ff..2964620 100644 --- a/tests/fixtures/parity/expected/assess__deep_tier_hit.json +++ b/tests/fixtures/parity/expected/assess__deep_tier_hit.json @@ -21,6 +21,27 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "Water boils at 100C.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "The claim is verified.", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": "https://lenz.io/api/v1/verifications/f3436f50" + } + ], + "error": null, + "more_claims": [] + }, "render": "True (high) — Water boils at 100C.\n The claim is verified.\n ask follow-ups: lenz ask f3436f50 \"\"\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Water boils at 100C.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": \"https://lenz.io/api/v1/verifications/f3436f50\",\n \"rationale\": \"The claim is verified.\",\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Water boils at 100C.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": \"https://lenz.io/api/v1/verifications/f3436f50\",\n \"rationale\": \"The claim is verified.\",\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='Water boils at 100C.', language='en', verdict='True', confidence='high', verification_url='https://lenz.io/api/v1/verifications/f3436f50', rationale='The claim is verified.', dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__idempotency_first.json b/tests/fixtures/parity/expected/assess__idempotency_first.json index cb3d6d5..614661f 100644 --- a/tests/fixtures/parity/expected/assess__idempotency_first.json +++ b/tests/fixtures/parity/expected/assess__idempotency_first.json @@ -21,6 +21,27 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "The Earth orbits the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "True (high) — The Earth orbits the Sun.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Earth orbits the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Earth orbits the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='The Earth orbits the Sun.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__idempotency_replay.json b/tests/fixtures/parity/expected/assess__idempotency_replay.json index cb3d6d5..614661f 100644 --- a/tests/fixtures/parity/expected/assess__idempotency_replay.json +++ b/tests/fixtures/parity/expected/assess__idempotency_replay.json @@ -21,6 +21,27 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "The Earth orbits the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "True (high) — The Earth orbits the Sun.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Earth orbits the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Earth orbits the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='The Earth orbits the Sun.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__list_all_error_rows.json b/tests/fixtures/parity/expected/assess__list_all_error_rows.json index edf8955..5617abd 100644 --- a/tests/fixtures/parity/expected/assess__list_all_error_rows.json +++ b/tests/fixtures/parity/expected/assess__list_all_error_rows.json @@ -35,6 +35,41 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "hi", + "confidence": "low", + "dissent": null, + "error_code": "no_claim", + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "hello", + "confidence": "low", + "dissent": null, + "error_code": "no_claim", + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "Error (no_claim) — hi\n The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\nError (no_claim) — hello\n The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"hi\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\"\n },\n {\n \"claim\": \"hello\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\"\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"hi\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\"\n },\n {\n \"claim\": \"hello\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\"\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='hi', language='en', verdict='Error', confidence='low', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code='no_claim', candidate_claims=[], identified_claims=[], hint='The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.'), AssessClaim(claim='hello', language='en', verdict='Error', confidence='low', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code='no_claim', candidate_claims=[], identified_claims=[], hint='The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.')], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__list_compound_item.json b/tests/fixtures/parity/expected/assess__list_compound_item.json index 6437e79..550432b 100644 --- a/tests/fixtures/parity/expected/assess__list_compound_item.json +++ b/tests/fixtures/parity/expected/assess__list_compound_item.json @@ -38,6 +38,44 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "Primary claim.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": "Assessed the main claim only. Send identified_claims as their own items to check the rest.", + "identified_claims": [ + "Second claim.", + "Third claim." + ], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Mostly False", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "A plain claim.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Mostly False", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "Mostly False (high) — Primary claim.\n also found:\n • Second claim.\n • Third claim.\n Assessed the main claim only. Send identified_claims as their own items to check the rest.\nMostly False (high) — A plain claim.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Primary claim.\",\n \"language\": \"en\",\n \"verdict\": \"Mostly False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [\n \"Second claim.\",\n \"Third claim.\"\n ],\n \"hint\": \"Assessed the main claim only. Send identified_claims as their own items to check the rest.\"\n },\n {\n \"claim\": \"A plain claim.\",\n \"language\": \"en\",\n \"verdict\": \"Mostly False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Primary claim.\",\n \"language\": \"en\",\n \"verdict\": \"Mostly False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [\n \"Second claim.\",\n \"Third claim.\"\n ],\n \"hint\": \"Assessed the main claim only. Send identified_claims as their own items to check the rest.\"\n },\n {\n \"claim\": \"A plain claim.\",\n \"language\": \"en\",\n \"verdict\": \"Mostly False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='Primary claim.', language='en', verdict='Mostly False', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=['Second claim.', 'Third claim.'], hint='Assessed the main claim only. Send identified_claims as their own items to check the rest.'), AssessClaim(claim='A plain claim.', language='en', verdict='Mostly False', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__list_mixed_rows.json b/tests/fixtures/parity/expected/assess__list_mixed_rows.json index f92df93..edb640a 100644 --- a/tests/fixtures/parity/expected/assess__list_mixed_rows.json +++ b/tests/fixtures/parity/expected/assess__list_mixed_rows.json @@ -63,6 +63,69 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "Water boils at 100 C at sea level.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "hello there", + "confidence": "low", + "dissent": null, + "error_code": "no_claim", + "hint": "The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "vendor is down", + "confidence": "low", + "dissent": null, + "error_code": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "cannot frame", + "confidence": "low", + "dissent": null, + "error_code": "framing_failed", + "hint": "This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; nothing was charged.", + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Error", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "True (high) — Water boils at 100 C at sea level.\nError (no_claim) — hello there\n The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\nError (upstream_unavailable) — vendor is down\n A model provider was unavailable for this item. Retry it; nothing was charged.\nError (framing_failed) — cannot frame\n This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; \nnothing was charged.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Water boils at 100 C at sea level.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"hello there\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\"\n },\n {\n \"claim\": \"vendor is down\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"upstream_unavailable\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"A model provider was unavailable for this item. Retry it; nothing was charged.\"\n },\n {\n \"claim\": \"cannot frame\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"framing_failed\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; nothing was charged.\"\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Water boils at 100 C at sea level.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"hello there\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.\"\n },\n {\n \"claim\": \"vendor is down\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"upstream_unavailable\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"A model provider was unavailable for this item. Retry it; nothing was charged.\"\n },\n {\n \"claim\": \"cannot frame\",\n \"language\": \"en\",\n \"verdict\": \"Error\",\n \"confidence\": \"low\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": \"framing_failed\",\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": \"This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; nothing was charged.\"\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='Water boils at 100 C at sea level.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='hello there', language='en', verdict='Error', confidence='low', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code='no_claim', candidate_claims=[], identified_claims=[], hint='The input is a greeting. Send one factual claim, or run the text through /extract to enumerate its claims.'), AssessClaim(claim='vendor is down', language='en', verdict='Error', confidence='low', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code='upstream_unavailable', candidate_claims=[], identified_claims=[], hint='A model provider was unavailable for this item. Retry it; nothing was charged.'), AssessClaim(claim='cannot frame', language='en', verdict='Error', confidence='low', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code='framing_failed', candidate_claims=[], identified_claims=[], hint='This item could not be processed; retrying it as-is will not help. Send it rephrased as one factual statement; nothing was charged.')], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__memo_hit.json b/tests/fixtures/parity/expected/assess__memo_hit.json index df5db6d..9fad459 100644 --- a/tests/fixtures/parity/expected/assess__memo_hit.json +++ b/tests/fixtures/parity/expected/assess__memo_hit.json @@ -21,6 +21,27 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "The Danube flows through Vienna.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "The river runs through the city.", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "True (high) — The Danube flows through Vienna.\n The river runs through the city.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Danube flows through Vienna.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"The river runs through the city.\",\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Danube flows through Vienna.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"The river runs through the city.\",\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='The Danube flows through Vienna.', language='en', verdict='True', confidence='high', verification_url=None, rationale='The river runs through the city.', dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__replay_later_200.json b/tests/fixtures/parity/expected/assess__replay_later_200.json index cb3d6d5..614661f 100644 --- a/tests/fixtures/parity/expected/assess__replay_later_200.json +++ b/tests/fixtures/parity/expected/assess__replay_later_200.json @@ -21,6 +21,27 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "The Earth orbits the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "True (high) — The Earth orbits the Sun.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Earth orbits the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The Earth orbits the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='The Earth orbits the Sun.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__single_no_claim.json b/tests/fixtures/parity/expected/assess__single_no_claim.json index 2c5225e..71489d8 100644 --- a/tests/fixtures/parity/expected/assess__single_no_claim.json +++ b/tests/fixtures/parity/expected/assess__single_no_claim.json @@ -6,6 +6,14 @@ "error_code": "no_claim", "more_claims": [] }, + "dump_unset": { + "candidate_claims": [], + "claims": [], + "error": "No verifiable claim detected", + "error_code": "no_claim", + "more_claims": [] + }, "render": "No claim found.\n", - "render_json": "{\n \"claims\": [],\n \"error\": \"No verifiable claim detected\",\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [],\n \"error\": \"No verifiable claim detected\",\n \"error_code\": \"no_claim\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[], error='No verifiable claim detected', error_code='no_claim', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__single_one_claim.json b/tests/fixtures/parity/expected/assess__single_one_claim.json index 6a00522..a5ad7b9 100644 --- a/tests/fixtures/parity/expected/assess__single_one_claim.json +++ b/tests/fixtures/parity/expected/assess__single_one_claim.json @@ -21,6 +21,27 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "The registry reported 4,200 filings in 2024.", + "confidence": "high", + "dissent": "The registry figure is provisional, so the 2024 total cannot yet be confirmed.", + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "The registry lists 4,200 filings for 2024 and has not revised the figure.", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "True (high) — The registry reported 4,200 filings in 2024.\n The registry lists 4,200 filings for 2024 and has not revised the figure.\n One reviewer disagreed: The registry figure is provisional, so the 2024 total cannot yet be confirmed.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The registry reported 4,200 filings in 2024.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"The registry lists 4,200 filings for 2024 and has not revised the figure.\",\n \"dissent\": \"The registry figure is provisional, so the 2024 total cannot yet be confirmed.\",\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"The registry reported 4,200 filings in 2024.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"The registry lists 4,200 filings for 2024 and has not revised the figure.\",\n \"dissent\": \"The registry figure is provisional, so the 2024 total cannot yet be confirmed.\",\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='The registry reported 4,200 filings in 2024.', language='en', verdict='True', confidence='high', verification_url=None, rationale='The registry lists 4,200 filings for 2024 and has not revised the figure.', dissent='The registry figure is provisional, so the 2024 total cannot yet be confirmed.', suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__single_text_over_wave_more_claims.json b/tests/fixtures/parity/expected/assess__single_text_over_wave_more_claims.json index 519a6b2..1a19a79 100644 --- a/tests/fixtures/parity/expected/assess__single_text_over_wave_more_claims.json +++ b/tests/fixtures/parity/expected/assess__single_text_over_wave_more_claims.json @@ -290,6 +290,296 @@ "Claim number 21 is documented." ] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "Claim number 0 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 1 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 2 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 3 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 4 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 5 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 6 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 7 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 8 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 9 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 10 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 11 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 12 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 13 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 14 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 15 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 16 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 17 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 18 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Claim number 19 is documented.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "more_claims": [ + "Claim number 20 is documented.", + "Claim number 21 is documented." + ] + }, "render": "True (high) — Claim number 0 is documented.\nTrue (high) — Claim number 1 is documented.\nTrue (high) — Claim number 2 is documented.\nTrue (high) — Claim number 3 is documented.\nTrue (high) — Claim number 4 is documented.\nTrue (high) — Claim number 5 is documented.\nTrue (high) — Claim number 6 is documented.\nTrue (high) — Claim number 7 is documented.\nTrue (high) — Claim number 8 is documented.\nTrue (high) — Claim number 9 is documented.\nTrue (high) — Claim number 10 is documented.\nTrue (high) — Claim number 11 is documented.\nTrue (high) — Claim number 12 is documented.\nTrue (high) — Claim number 13 is documented.\nTrue (high) — Claim number 14 is documented.\nTrue (high) — Claim number 15 is documented.\nTrue (high) — Claim number 16 is documented.\nTrue (high) — Claim number 17 is documented.\nTrue (high) — Claim number 18 is documented.\nTrue (high) — Claim number 19 is documented.\n\nNot checked (2 more claims found; pass them to `lenz assess`, 20 a call):\n • Claim number 20 is documented.\n • Claim number 21 is documented.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Claim number 0 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 1 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 2 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 3 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 4 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 5 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 6 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 7 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 8 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 9 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 10 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 11 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 12 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 13 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 14 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 15 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 16 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 17 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 18 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 19 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": [\n \"Claim number 20 is documented.\",\n \"Claim number 21 is documented.\"\n ]\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Claim number 0 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 1 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 2 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 3 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 4 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 5 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 6 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 7 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 8 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 9 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 10 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 11 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 12 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 13 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 14 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 15 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 16 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 17 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 18 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Claim number 19 is documented.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": [\n \"Claim number 20 is documented.\",\n \"Claim number 21 is documented.\"\n ]\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='Claim number 0 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 1 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 2 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 3 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 4 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 5 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 6 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 7 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 8 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 9 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 10 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 11 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 12 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 13 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 14 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 15 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 16 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 17 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 18 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Claim number 19 is documented.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=['Claim number 20 is documented.', 'Claim number 21 is documented.'])" } diff --git a/tests/fixtures/parity/expected/assess__single_text_several_claims.json b/tests/fixtures/parity/expected/assess__single_text_several_claims.json index 62e14ad..c3d142e 100644 --- a/tests/fixtures/parity/expected/assess__single_text_several_claims.json +++ b/tests/fixtures/parity/expected/assess__single_text_several_claims.json @@ -49,6 +49,55 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "Water boils at 100 C at sea level.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "The Moon is made of cheese.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "The Pacific is the deepest ocean.", + "confidence": "medium", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": null, + "suggested_rewrite": null, + "verdict": "Mostly True", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "True (high) — Water boils at 100 C at sea level.\nFalse (high) — The Moon is made of cheese.\nMostly True (medium) — The Pacific is the deepest ocean.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Water boils at 100 C at sea level.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"The Moon is made of cheese.\",\n \"language\": \"en\",\n \"verdict\": \"False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"The Pacific is the deepest ocean.\",\n \"language\": \"en\",\n \"verdict\": \"Mostly True\",\n \"confidence\": \"medium\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Water boils at 100 C at sea level.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"The Moon is made of cheese.\",\n \"language\": \"en\",\n \"verdict\": \"False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"The Pacific is the deepest ocean.\",\n \"language\": \"en\",\n \"verdict\": \"Mostly True\",\n \"confidence\": \"medium\",\n \"verification_url\": null,\n \"rationale\": null,\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='Water boils at 100 C at sea level.', language='en', verdict='True', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='The Moon is made of cheese.', language='en', verdict='False', confidence='high', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='The Pacific is the deepest ocean.', language='en', verdict='Mostly True', confidence='medium', verification_url=None, rationale=None, dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/assess__suggest_rewrite.json b/tests/fixtures/parity/expected/assess__suggest_rewrite.json index da850bb..e24cfab 100644 --- a/tests/fixtures/parity/expected/assess__suggest_rewrite.json +++ b/tests/fixtures/parity/expected/assess__suggest_rewrite.json @@ -35,6 +35,41 @@ "error_code": "", "more_claims": [] }, + "dump_unset": { + "claims": [ + { + "candidate_claims": [], + "claim": "Venus is the closest planet to the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "Mercury, not Venus, is the closest planet to the Sun.", + "suggested_rewrite": "Mercury is the closest planet to the Sun.", + "verdict": "False", + "verification_url": null + }, + { + "candidate_claims": [], + "claim": "Mercury is the closest planet to the Sun.", + "confidence": "high", + "dissent": null, + "error_code": null, + "hint": null, + "identified_claims": [], + "language": "en", + "rationale": "Mercury is the innermost planet.", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + } + ], + "error": null, + "more_claims": [] + }, "render": "False (high) — Venus is the closest planet to the Sun.\n Mercury, not Venus, is the closest planet to the Sun.\nTrue (high) — Mercury is the closest planet to the Sun.\n Mercury is the innermost planet.\n", - "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Venus is the closest planet to the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"Mercury, not Venus, is the closest planet to the Sun.\",\n \"dissent\": null,\n \"suggested_rewrite\": \"Mercury is the closest planet to the Sun.\",\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Mercury is the closest planet to the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"Mercury is the innermost planet.\",\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n" + "render_json": "{\n \"claims\": [\n {\n \"claim\": \"Venus is the closest planet to the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"False\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"Mercury, not Venus, is the closest planet to the Sun.\",\n \"dissent\": null,\n \"suggested_rewrite\": \"Mercury is the closest planet to the Sun.\",\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n },\n {\n \"claim\": \"Mercury is the closest planet to the Sun.\",\n \"language\": \"en\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"verification_url\": null,\n \"rationale\": \"Mercury is the innermost planet.\",\n \"dissent\": null,\n \"suggested_rewrite\": null,\n \"error_code\": null,\n \"candidate_claims\": [],\n \"identified_claims\": [],\n \"hint\": null\n }\n ],\n \"error\": null,\n \"error_code\": \"\",\n \"candidate_claims\": [],\n \"more_claims\": []\n}\n", + "repr": "AssessResponse(claims=[AssessClaim(claim='Venus is the closest planet to the Sun.', language='en', verdict='False', confidence='high', verification_url=None, rationale='Mercury, not Venus, is the closest planet to the Sun.', dissent=None, suggested_rewrite='Mercury is the closest planet to the Sun.', error_code=None, candidate_claims=[], identified_claims=[], hint=None), AssessClaim(claim='Mercury is the closest planet to the Sun.', language='en', verdict='True', confidence='high', verification_url=None, rationale='Mercury is the innermost planet.', dissent=None, suggested_rewrite=None, error_code=None, candidate_claims=[], identified_claims=[], hint=None)], error=None, error_code='', candidate_claims=[], more_claims=[])" } diff --git a/tests/fixtures/parity/expected/citecheck__402_no_credits.json b/tests/fixtures/parity/expected/citecheck__402_no_credits.json index 9d5ec22..add7b94 100644 --- a/tests/fixtures/parity/expected/citecheck__402_no_credits.json +++ b/tests/fixtures/parity/expected/citecheck__402_no_credits.json @@ -20,7 +20,7 @@ "remaining": 100, "request_id": "", "requested": null, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/citecheck__422_webhook_secret_missing.json b/tests/fixtures/parity/expected/citecheck__422_webhook_secret_missing.json new file mode 100644 index 0000000..aa5a20c --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__422_webhook_secret_missing.json @@ -0,0 +1,30 @@ +{ + "error": { + "cause": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "code": "webhook_secret_missing", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "webhook_url" + ], + "msg": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.\n Fix: Check the request body against the OpenAPI spec.", + "message": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/citecheck__get_checking.json b/tests/fixtures/parity/expected/citecheck__get_checking.json index c5352dd..2490a65 100644 --- a/tests/fixtures/parity/expected/citecheck__get_checking.json +++ b/tests/fixtures/parity/expected/citecheck__get_checking.json @@ -68,7 +68,102 @@ ], "citecheck_id": "8eb49302", "completed_at": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": null, + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": 10, + "status": "checking", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": null, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": null, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": null, + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -96,5 +191,6 @@ }, "exit_code": 2, "render": "Citation check 8eb49302: checking — 0 credits charged\n\n2 sources cited in your draft\nLenz checks each one: does the source say what your draft says it does.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='checking', outcome=None, created_at='2026-10-01T09:07:00.123456Z', completed_at=None, poll_after_seconds=10, policy=CitecheckPolicy(max_citations=2), summary=CitecheckSummary(citations_found=2, citations_selected=2, citation_limit=2, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=0, unchecked=0, failed=0), citation_issues=0), credits=ReviewCredits(charged=0), citations=[ReviewCitation(index=0, reference='https://example.gov/report-2024', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024.', quotes=[], position=None, result=None, check=ReviewCitationCheck(status='pending', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='https://example.org/statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=None, result=None, check=ReviewCitationCheck(status='pending', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))], citation_issues=[], citation_failures=[], more_citations=[], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_clean.json b/tests/fixtures/parity/expected/citecheck__get_completed_clean.json index 0b78c9e..e1cfe30 100644 --- a/tests/fixtures/parity/expected/citecheck__get_completed_clean.json +++ b/tests/fixtures/parity/expected/citecheck__get_completed_clean.json @@ -75,8 +75,111 @@ } ], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "clean", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -104,5 +207,6 @@ }, "exit_code": 0, "render": "Citation check 8eb49302: clean — 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=2), summary=CitecheckSummary(citations_found=2, citations_selected=2, citation_limit=2, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=0), credits=ReviewCredits(charged=2), citations=[ReviewCitation(index=0, reference='https://example.gov/report-2024', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024.', quotes=[], position=None, result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='https://example.org/statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=None, result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))], citation_issues=[], citation_failures=[], more_citations=[], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_doi_pair.json b/tests/fixtures/parity/expected/citecheck__get_completed_doi_pair.json index 1857179..e793493 100644 --- a/tests/fixtures/parity/expected/citecheck__get_completed_doi_pair.json +++ b/tests/fixtures/parity/expected/citecheck__get_completed_doi_pair.json @@ -40,8 +40,76 @@ } ], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "clean", + "policy": { + "max_citations": 1 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 1, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 1, + "citation_limit_reached": false, + "citations_found": 1, + "citations_selected": 1 + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at None", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": null, + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://doi.org/10.1038/nature12373", + "doi": "10.1038/nature12373", + "index": 0, + "position": null, + "quotes": [], + "reference": "10.1038/nature12373", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Diamond sensors measure heat in cells." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -69,5 +137,6 @@ }, "exit_code": 0, "render": "Citation check 8eb49302: clean — 1 credit charged\n\n1 source cited in your draft\n1 checked.\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=1), summary=CitecheckSummary(citations_found=1, citations_selected=1, citation_limit=1, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=1, unchecked=0, failed=0), citation_issues=0), credits=ReviewCredits(charged=1), citations=[ReviewCitation(index=0, reference='10.1038/nature12373', cited_url='https://doi.org/10.1038/nature12373', doi='10.1038/nature12373', statement='Diamond sensors measure heat in cells.', quotes=[], position=None, result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at None', page_published_date=None, page_language='en', source_url=None, source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))], citation_issues=[], citation_failures=[], more_citations=[], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_incomplete.json b/tests/fixtures/parity/expected/citecheck__get_completed_incomplete.json index 267612f..9a2c57c 100644 --- a/tests/fixtures/parity/expected/citecheck__get_completed_incomplete.json +++ b/tests/fixtures/parity/expected/citecheck__get_completed_incomplete.json @@ -91,8 +91,127 @@ } ], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "incomplete", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 1, + "failed": 1, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "dump_unset": { + "citation_failures": [ + { + "citation_index": 1, + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#internal", + "failure_class": "internal", + "failure_reason": "internal", + "hint": "The check failed on our side. Try again later.", + "retryable": false + }, + "reference": "https://example.org/statement" + } + ], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#internal", + "failure_class": "internal", + "failure_reason": "internal", + "hint": "The check failed on our side. Try again later.", + "retryable": false + }, + "hint": "The check failed on our side. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": null, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -120,5 +239,6 @@ }, "exit_code": 2, "render": "Citation check 8eb49302: incomplete — 1 credit charged\n\n2 sources cited in your draft\n1 checked. 1 could not be checked.\n\n[source 2/2] Could not be checked this time.\n https://example.org/statement\n The check failed on our side. Try again later.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='completed', outcome='incomplete', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=2), summary=CitecheckSummary(citations_found=2, citations_selected=2, citation_limit=2, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=1, unchecked=0, failed=1), citation_issues=0), credits=ReviewCredits(charged=1), citations=[ReviewCitation(index=0, reference='https://example.gov/report-2024', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024.', quotes=[], position=None, result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='https://example.org/statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=None, result=None, check=ReviewCitationCheck(status='failed', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint='The check failed on our side. Try again later.', failure=FailureBlock(failure_reason='internal', failure_class='internal', retryable=False, hint='The check failed on our side. Try again later.', docs_url='https://lenz.io/docs/errors#internal')))], citation_issues=[], citation_failures=[ReviewCitationFailure(citation_index=1, reference='https://example.org/statement', cited_url='https://example.org/statement', doi=None, failure=FailureBlock(failure_reason='internal', failure_class='internal', retryable=False, hint='The check failed on our side. Try again later.', docs_url='https://lenz.io/docs/errors#internal'))], more_citations=[], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_issues_found.json b/tests/fixtures/parity/expected/citecheck__get_completed_issues_found.json index 0bee066..2d22ac8 100644 --- a/tests/fixtures/parity/expected/citecheck__get_completed_issues_found.json +++ b/tests/fixtures/parity/expected/citecheck__get_completed_issues_found.json @@ -105,8 +105,141 @@ } ], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "issues_found", + "policy": { + "max_citations": 20 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 1, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": null, + "finding": "contradicted", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "rationale": "The page states it.", + "reference": "a report from the ministry", + "snippet": "The source says so.", + "source": "support", + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "contradicted", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "contradicted", + "is_issue": true, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -134,5 +267,6 @@ }, "exit_code": 1, "render": "Citation check 8eb49302: issues found — 2 credits charged\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='completed', outcome='issues_found', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=20), summary=CitecheckSummary(citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=1), credits=ReviewCredits(charged=2), citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='contradicted', source='support', is_issue=True), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='contradicted', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=Position(start=111, end=178, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))], citation_issues=[ReviewCitationIssue(citation_index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), finding='contradicted', source='support', snippet='The source says so.', rationale='The page states it.', missing_quote=None, metadata_differences=[], page_title='Page at https://example.gov/report-2024', failure=None)], citation_failures=[], more_citations=[], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_partly_supported.json b/tests/fixtures/parity/expected/citecheck__get_completed_partly_supported.json index 3d302ef..8077d22 100644 --- a/tests/fixtures/parity/expected/citecheck__get_completed_partly_supported.json +++ b/tests/fixtures/parity/expected/citecheck__get_completed_partly_supported.json @@ -75,8 +75,111 @@ } ], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "clean", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "partly_supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": { + "finding": "partly_supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -104,5 +207,6 @@ }, "exit_code": 0, "render": "Citation check 8eb49302: clean — 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=2), summary=CitecheckSummary(citations_found=2, citations_selected=2, citation_limit=2, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=0), credits=ReviewCredits(charged=2), citations=[ReviewCitation(index=0, reference='https://example.gov/report-2024', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024.', quotes=[], position=None, result=ReviewCitationResult(finding='partly_supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='partly_supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='https://example.org/statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=None, result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))], citation_issues=[], citation_failures=[], more_citations=[], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_completed_unchecked.json b/tests/fixtures/parity/expected/citecheck__get_completed_unchecked.json index 4e9a931..b5c061f 100644 --- a/tests/fixtures/parity/expected/citecheck__get_completed_unchecked.json +++ b/tests/fixtures/parity/expected/citecheck__get_completed_unchecked.json @@ -75,8 +75,111 @@ } ], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "unchecked", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 0, + "unchecked": 2 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": null, + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "unchecked", + "unchecked_reason": "no_text" + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": { + "finding": "unchecked", + "is_issue": false, + "source": null + }, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": null, + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "unchecked", + "unchecked_reason": "no_text" + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": { + "finding": "unchecked", + "is_issue": false, + "source": null + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -104,5 +207,6 @@ }, "exit_code": 2, "render": "Citation check 8eb49302: unchecked — 0 credits charged\n\n2 sources cited in your draft\n2 could not be checked.\n\nCheck these by hand (Lenz could not check them)\n[source 1/2] The page gave no text to read.\n https://example.gov/report-2024\n[source 2/2] The page gave no text to read.\n https://example.org/statement\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='completed', outcome='unchecked', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=2), summary=CitecheckSummary(citations_found=2, citations_selected=2, citation_limit=2, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=0, unchecked=2, failed=0), citation_issues=0), credits=ReviewCredits(charged=0), citations=[ReviewCitation(index=0, reference='https://example.gov/report-2024', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024.', quotes=[], position=None, result=ReviewCitationResult(finding='unchecked', source=None, is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='unchecked', snippet=None, rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason='no_text', hint=None, failure=None)), ReviewCitation(index=1, reference='https://example.org/statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=None, result=ReviewCitationResult(finding='unchecked', source=None, is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='unchecked', snippet=None, rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason='no_text', hint=None, failure=None))], citation_issues=[], citation_failures=[], more_citations=[], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_failed_no_citations.json b/tests/fixtures/parity/expected/citecheck__get_failed_no_citations.json index 2fcc755..fbc25bf 100644 --- a/tests/fixtures/parity/expected/citecheck__get_failed_no_citations.json +++ b/tests/fixtures/parity/expected/citecheck__get_failed_no_citations.json @@ -4,8 +4,42 @@ "citation_issues": [], "citations": [], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_citations", + "hint": "The text has no citation to check: no link, DOI or numbered reference with one.", + "retryable": false + }, + "language": "en", + "more_citations": null, + "outcome": "unchecked", + "policy": { + "max_citations": 20 + }, + "poll_after_seconds": null, + "status": "failed", + "summary": { + "citation_checks": null, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -35,5 +69,6 @@ }, "exit_code": 2, "render": "Citation check 8eb49302: unchecked — 0 credits charged\nFailed: The text has no citation to check: no link, DOI or numbered reference with one.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='failed', outcome='unchecked', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=20), summary=CitecheckSummary(citations_found=None, citations_selected=None, citation_limit=20, citation_limit_reached=None, citation_checks=None, citation_issues=0), credits=ReviewCredits(charged=0), citations=[], citation_issues=[], citation_failures=[], more_citations=None, failure=FailureBlock(failure_reason='no_citations', failure_class='invalid_input', retryable=False, hint='The text has no citation to check: no link, DOI or numbered reference with one.', docs_url='https://lenz.io/docs/errors#invalid-input'), language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_failed_upstream_unavailable.json b/tests/fixtures/parity/expected/citecheck__get_failed_upstream_unavailable.json index 81d92d8..8365eb9 100644 --- a/tests/fixtures/parity/expected/citecheck__get_failed_upstream_unavailable.json +++ b/tests/fixtures/parity/expected/citecheck__get_failed_upstream_unavailable.json @@ -106,8 +106,148 @@ } ], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": null, + "retryable": true + }, + "language": "en", + "more_citations": [], + "outcome": "unchecked", + "policy": { + "max_citations": 20 + }, + "poll_after_seconds": null, + "status": "failed", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 2, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "dump_unset": { + "citation_failures": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "Lenz could not reach this source just now. Try again later.", + "retryable": true + }, + "reference": "a report from the ministry" + }, + { + "citation_index": 1, + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "Lenz could not reach this source just now. Try again later.", + "retryable": true + }, + "reference": "a statement" + } + ], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "Lenz could not reach this source just now. Try again later.", + "retryable": true + }, + "hint": "Lenz could not reach this source just now. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "a report from the ministry", + "result": null, + "statement": null + }, + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "Lenz could not reach this source just now. Try again later.", + "retryable": true + }, + "hint": "Lenz could not reach this source just now. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "a statement", + "result": null, + "statement": null + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -141,5 +281,6 @@ }, "exit_code": 2, "render": "Citation check 8eb49302: unchecked — 0 credits charged\nFailed: upstream_unavailable\n\n2 sources cited in your draft\n2 could not be checked.\n\n[source 1/2] Could not be checked this time.\n https://example.gov/report-2024\n Lenz could not reach this source just now. Try again later.\n\n[source 2/2] Could not be checked this time.\n https://example.org/statement\n Lenz could not reach this source just now. Try again later.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='failed', outcome='unchecked', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=20), summary=CitecheckSummary(citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=0, unchecked=0, failed=2), citation_issues=0), credits=ReviewCredits(charged=0), citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement=None, quotes=[], position=None, result=None, check=ReviewCitationCheck(status='failed', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint='Lenz could not reach this source just now. Try again later.', failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='Lenz could not reach this source just now. Try again later.', docs_url='https://lenz.io/docs/errors#upstream-unavailable'))), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement=None, quotes=[], position=None, result=None, check=ReviewCitationCheck(status='failed', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint='Lenz could not reach this source just now. Try again later.', failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='Lenz could not reach this source just now. Try again later.', docs_url='https://lenz.io/docs/errors#upstream-unavailable')))], citation_issues=[], citation_failures=[ReviewCitationFailure(citation_index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='Lenz could not reach this source just now. Try again later.', docs_url='https://lenz.io/docs/errors#upstream-unavailable')), ReviewCitationFailure(citation_index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='Lenz could not reach this source just now. Try again later.', docs_url='https://lenz.io/docs/errors#upstream-unavailable'))], more_citations=[], failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint=None, docs_url='https://lenz.io/docs/errors#upstream-unavailable'), language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_queued.json b/tests/fixtures/parity/expected/citecheck__get_queued.json index 402fbe9..b74baa6 100644 --- a/tests/fixtures/parity/expected/citecheck__get_queued.json +++ b/tests/fixtures/parity/expected/citecheck__get_queued.json @@ -37,7 +37,71 @@ ], "citecheck_id": "8eb49302", "completed_at": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": null, + "policy": { + "max_citations": 1 + }, + "poll_after_seconds": 10, + "status": "queued", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 1, + "citation_limit_reached": false, + "citations_found": 1, + "citations_selected": 1 + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": null, + "statement": "Unemployment fell to 4.1% in 2024." + } + ], + "citecheck_id": "8eb49302", + "completed_at": null, + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -65,5 +129,6 @@ }, "exit_code": 2, "render": "Citation check 8eb49302: queued — 0 credits charged\n\n1 source cited in your draft\nLenz checks each one: does the source say what your draft says it does.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='queued', outcome=None, created_at='2026-10-01T09:07:00.123456Z', completed_at=None, poll_after_seconds=10, policy=CitecheckPolicy(max_citations=1), summary=CitecheckSummary(citations_found=1, citations_selected=1, citation_limit=1, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=0, unchecked=0, failed=0), citation_issues=0), credits=ReviewCredits(charged=0), citations=[ReviewCitation(index=0, reference='https://example.gov/report-2024', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024.', quotes=[], position=None, result=None, check=ReviewCitationCheck(status='pending', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))], citation_issues=[], citation_failures=[], more_citations=[], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_text_limit_exactly_at_limit.json b/tests/fixtures/parity/expected/citecheck__get_text_limit_exactly_at_limit.json index 174ac25..f3818cf 100644 --- a/tests/fixtures/parity/expected/citecheck__get_text_limit_exactly_at_limit.json +++ b/tests/fixtures/parity/expected/citecheck__get_text_limit_exactly_at_limit.json @@ -83,8 +83,119 @@ } ], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "language": "en", + "more_citations": [], + "outcome": "clean", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -112,5 +223,6 @@ }, "exit_code": 0, "render": "Citation check 8eb49302: clean — 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=2), summary=CitecheckSummary(citations_found=2, citations_selected=2, citation_limit=2, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=0), credits=ReviewCredits(charged=2), citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=Position(start=111, end=178, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))], citation_issues=[], citation_failures=[], more_citations=[], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__get_text_limit_reached.json b/tests/fixtures/parity/expected/citecheck__get_text_limit_reached.json index b97967a..bd075a8 100644 --- a/tests/fixtures/parity/expected/citecheck__get_text_limit_reached.json +++ b/tests/fixtures/parity/expected/citecheck__get_text_limit_reached.json @@ -44,8 +44,93 @@ } ], "citecheck_id": "8eb49302", - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "language": "en", + "more_citations": [ + { + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "reference": "a statement", + "sentence": "The agency said so in a statement." + } + ], + "outcome": "clean", + "policy": { + "max_citations": 1 + }, + "poll_after_seconds": null, + "status": "completed", + "summary": { + "citation_checks": { + "checked": 1, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 1, + "citation_limit_reached": true, + "citations_found": 2, + "citations_selected": 1 + } + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -86,5 +171,6 @@ }, "exit_code": 0, "render": "Citation check 8eb49302: clean — 1 credit charged\n\n2 sources cited in your draft\n1 checked.\n1 more was not checked: one check covers 1.\n\nNo issues.\n\n1 more citation was found but not checked.\n", - "render_json": null + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=1), summary=CitecheckSummary(citations_found=2, citations_selected=1, citation_limit=1, citation_limit_reached=True, citation_checks=ReviewCitationCheckCounts(checked=1, unchecked=0, failed=0), citation_issues=0), credits=ReviewCredits(charged=1), citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))], citation_issues=[], citation_failures=[], more_citations=[ReviewMoreCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, sentence='The agency said so in a statement.', position=Position(start=111, end=178, text=None))], failure=None, language='en')" } diff --git a/tests/fixtures/parity/expected/citecheck__idempotent_replay_202.json b/tests/fixtures/parity/expected/citecheck__idempotent_replay_202.json index 6ebcb9b..a0700ca 100644 --- a/tests/fixtures/parity/expected/citecheck__idempotent_replay_202.json +++ b/tests/fixtures/parity/expected/citecheck__idempotent_replay_202.json @@ -3,5 +3,10 @@ "citecheck_id": "8eb49302", "status": "queued" }, - "render_json": null + "dump_unset": { + "citecheck_id": "8eb49302", + "status": "queued" + }, + "render_json": null, + "repr": "CitecheckStarted(citecheck_id='8eb49302', status='queued')" } diff --git a/tests/fixtures/parity/expected/citecheck__receipt_202.json b/tests/fixtures/parity/expected/citecheck__receipt_202.json index 6ebcb9b..a0700ca 100644 --- a/tests/fixtures/parity/expected/citecheck__receipt_202.json +++ b/tests/fixtures/parity/expected/citecheck__receipt_202.json @@ -3,5 +3,10 @@ "citecheck_id": "8eb49302", "status": "queued" }, - "render_json": null + "dump_unset": { + "citecheck_id": "8eb49302", + "status": "queued" + }, + "render_json": null, + "repr": "CitecheckStarted(citecheck_id='8eb49302', status='queued')" } diff --git a/tests/fixtures/parity/expected/citecheck__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/expected/citecheck__receipt_202_empty_webhook_url.json index 6ebcb9b..a0700ca 100644 --- a/tests/fixtures/parity/expected/citecheck__receipt_202_empty_webhook_url.json +++ b/tests/fixtures/parity/expected/citecheck__receipt_202_empty_webhook_url.json @@ -3,5 +3,10 @@ "citecheck_id": "8eb49302", "status": "queued" }, - "render_json": null + "dump_unset": { + "citecheck_id": "8eb49302", + "status": "queued" + }, + "render_json": null, + "repr": "CitecheckStarted(citecheck_id='8eb49302', status='queued')" } diff --git a/tests/fixtures/parity/expected/errors__auth_bad_oauth_token.json b/tests/fixtures/parity/expected/errors__auth_bad_oauth_token.json new file mode 100644 index 0000000..5186317 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_bad_oauth_token.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "The access token is invalid, expired or revoked.", + "code": "invalid_token", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "The access token is invalid, expired or revoked.\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "The access token is invalid, expired or revoked.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "The access token is invalid, expired or revoked.", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_expired_oauth_token.json b/tests/fixtures/parity/expected/errors__auth_expired_oauth_token.json new file mode 100644 index 0000000..5186317 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_expired_oauth_token.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "The access token is invalid, expired or revoked.", + "code": "invalid_token", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "The access token is invalid, expired or revoked.\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "The access token is invalid, expired or revoked.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "The access token is invalid, expired or revoked.", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_oauth_issuer_off.json b/tests/fixtures/parity/expected/errors__auth_oauth_issuer_off.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_oauth_issuer_off.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__auth_oauth_only.json b/tests/fixtures/parity/expected/errors__auth_oauth_only.json new file mode 100644 index 0000000..99f0634 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__auth_oauth_only.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "code": "oauth_only", + "doc_url": "https://lenz.io/docs/auth", + "fix": "This key doesn't have access to that resource.", + "friendly_text": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.\n Fix: This key doesn't have access to that resource.", + "message": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "This key doesn't have access to that resource.", + "message": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "status": 403 + } + }, + "request_id": "", + "status_code": 403, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__method_not_allowed_get_verify.json b/tests/fixtures/parity/expected/errors__method_not_allowed_get_verify.json new file mode 100644 index 0000000..ebe2cee --- /dev/null +++ b/tests/fixtures/parity/expected/errors__method_not_allowed_get_verify.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 405", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 405\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 405", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 405", + "status": 405 + } + }, + "request_id": "", + "status_code": 405, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__method_not_allowed_post_library.json b/tests/fixtures/parity/expected/errors__method_not_allowed_post_library.json new file mode 100644 index 0000000..ebe2cee --- /dev/null +++ b/tests/fixtures/parity/expected/errors__method_not_allowed_post_library.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 405", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 405\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 405", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 405", + "status": 405 + } + }, + "request_id": "", + "status_code": 405, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__oauth_register_disabled.json b/tests/fixtures/parity/expected/errors__oauth_register_disabled.json new file mode 100644 index 0000000..8c9406d --- /dev/null +++ b/tests/fixtures/parity/expected/errors__oauth_register_disabled.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 404", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 404\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 404", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 404", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__oauth_register_invalid_client_metadata.json b/tests/fixtures/parity/expected/errors__oauth_register_invalid_client_metadata.json new file mode 100644 index 0000000..3ae0fc8 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__oauth_register_invalid_client_metadata.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 400", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 400\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "status": 400 + } + }, + "request_id": "", + "status_code": 400, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__oauth_register_invalid_redirect_uri.json b/tests/fixtures/parity/expected/errors__oauth_register_invalid_redirect_uri.json new file mode 100644 index 0000000..3ae0fc8 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__oauth_register_invalid_redirect_uri.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 400", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 400\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "status": 400 + } + }, + "request_id": "", + "status_code": 400, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__oauth_revoke_invalid_client.json b/tests/fixtures/parity/expected/errors__oauth_revoke_invalid_client.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__oauth_revoke_invalid_client.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__oauth_token_invalid_client.json b/tests/fixtures/parity/expected/errors__oauth_token_invalid_client.json new file mode 100644 index 0000000..e57982a --- /dev/null +++ b/tests/fixtures/parity/expected/errors__oauth_token_invalid_client.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Unauthorized", + "code": "", + "doc_url": "https://lenz.io/docs/auth", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "friendly_text": "Unauthorized\n Fix: Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", + "message": "Unauthorized", + "status": 401 + } + }, + "request_id": "", + "status_code": 401, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__oauth_token_invalid_grant.json b/tests/fixtures/parity/expected/errors__oauth_token_invalid_grant.json new file mode 100644 index 0000000..3ae0fc8 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__oauth_token_invalid_grant.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 400", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 400\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "status": 400 + } + }, + "request_id": "", + "status_code": 400, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__oauth_token_missing_grant_type.json b/tests/fixtures/parity/expected/errors__oauth_token_missing_grant_type.json new file mode 100644 index 0000000..3ae0fc8 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__oauth_token_missing_grant_type.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 400", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 400\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "status": 400 + } + }, + "request_id": "", + "status_code": 400, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__oauth_token_refresh_invalid_grant.json b/tests/fixtures/parity/expected/errors__oauth_token_refresh_invalid_grant.json new file mode 100644 index 0000000..3ae0fc8 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__oauth_token_refresh_invalid_grant.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 400", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 400\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "status": 400 + } + }, + "request_id": "", + "status_code": 400, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__oauth_token_unsupported_grant_type.json b/tests/fixtures/parity/expected/errors__oauth_token_unsupported_grant_type.json new file mode 100644 index 0000000..3ae0fc8 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__oauth_token_unsupported_grant_type.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 400", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 400\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 400", + "status": 400 + } + }, + "request_id": "", + "status_code": 400, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/errors__payment_required_ask.json b/tests/fixtures/parity/expected/errors__payment_required_ask.json index e519757..a635323 100644 --- a/tests/fixtures/parity/expected/errors__payment_required_ask.json +++ b/tests/fixtures/parity/expected/errors__payment_required_ask.json @@ -20,7 +20,7 @@ "remaining": 0, "request_id": "", "requested": null, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/errors__payment_required_assess.json b/tests/fixtures/parity/expected/errors__payment_required_assess.json index 732bc39..91af7cd 100644 --- a/tests/fixtures/parity/expected/errors__payment_required_assess.json +++ b/tests/fixtures/parity/expected/errors__payment_required_assess.json @@ -20,7 +20,7 @@ "remaining": 0, "request_id": "", "requested": null, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/errors__payment_required_verify.json b/tests/fixtures/parity/expected/errors__payment_required_verify.json index f3a12a3..f16364a 100644 --- a/tests/fixtures/parity/expected/errors__payment_required_verify.json +++ b/tests/fixtures/parity/expected/errors__payment_required_verify.json @@ -20,7 +20,7 @@ "remaining": 0, "request_id": "", "requested": null, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/errors__payment_required_verify_batch_short.json b/tests/fixtures/parity/expected/errors__payment_required_verify_batch_short.json index 4506242..21c7df3 100644 --- a/tests/fixtures/parity/expected/errors__payment_required_verify_batch_short.json +++ b/tests/fixtures/parity/expected/errors__payment_required_verify_batch_short.json @@ -20,7 +20,7 @@ "remaining": 3, "request_id": "", "requested": 5, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/errors__web_assess_payment_required.json b/tests/fixtures/parity/expected/errors__web_assess_payment_required.json new file mode 100644 index 0000000..04bef38 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__web_assess_payment_required.json @@ -0,0 +1,28 @@ +{ + "error": { + "cause": "No assessments left this month.", + "code": "no_credits", + "cost": 1, + "credit_balance": 0, + "doc_url": "https://lenz.io/docs/billing", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "friendly_text": "No assessments left this month.\n Fix: Top up or upgrade at https://lenz.io/plans, or wait for the period reset. See https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "message": "No assessments left this month.", + "payload_json": { + "error": { + "code": "no_credits", + "fix": "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + "message": "No assessments left this month.", + "status": 402, + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } + }, + "remaining": 0, + "request_id": "", + "requested": null, + "resets_at": "2026-10-01T14:07:00+00:00", + "status_code": 402, + "type": "LenzQuotaExceededError", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" + } +} diff --git a/tests/fixtures/parity/expected/errors__web_assess_signup_required.json b/tests/fixtures/parity/expected/errors__web_assess_signup_required.json new file mode 100644 index 0000000..b16a7bb --- /dev/null +++ b/tests/fixtures/parity/expected/errors__web_assess_signup_required.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Sign up to check this — it’s free.", + "code": "signup_required", + "doc_url": "https://lenz.io/docs/auth", + "fix": "This key doesn't have access to that resource.", + "friendly_text": "Sign up to check this — it’s free.\n Fix: This key doesn't have access to that resource.", + "message": "Sign up to check this — it’s free.", + "payload_json": { + "error": { + "code": "unauthorized", + "fix": "This key doesn't have access to that resource.", + "message": "Sign up to check this — it’s free.", + "status": 403 + } + }, + "request_id": "", + "status_code": 403, + "type": "LenzAuthError" + } +} diff --git a/tests/fixtures/parity/expected/errors__web_review_rate_limited.json b/tests/fixtures/parity/expected/errors__web_review_rate_limited.json new file mode 100644 index 0000000..6841e77 --- /dev/null +++ b/tests/fixtures/parity/expected/errors__web_review_rate_limited.json @@ -0,0 +1,25 @@ +{ + "error": { + "cause": "This account has used its extractions for today. The daily allowance is shared with the Lenz API. It resets at 00:00 UTC.", + "code": "extract_daily_limit", + "doc_url": "https://lenz.io/docs/rate-limits", + "fix": "Wait Retry-After seconds and retry.", + "friendly_text": "This account has used its extractions for today. The daily allowance is shared with the Lenz API. It resets at 00:00 UTC.\n Fix: Wait Retry-After seconds and retry.", + "limit": 1000, + "message": "This account has used its extractions for today. The daily allowance is shared with the Lenz API. It resets at 00:00 UTC.", + "payload_json": { + "error": { + "code": "rate_limited", + "fix": "Wait Retry-After seconds and retry.", + "message": "This account has used its extractions for today. The daily allowance is shared with the Lenz API. It resets at 00:00 UTC.", + "status": 429 + } + }, + "request_id": "", + "reset_in_seconds": 777, + "retry_after": 777, + "status_code": 429, + "type": "LenzRateLimitError", + "upgrade_url": "https://lenz.io/plans" + } +} diff --git a/tests/fixtures/parity/expected/extract__focus_ready.json b/tests/fixtures/parity/expected/extract__focus_ready.json index 0b529b1..19a1965 100644 --- a/tests/fixtures/parity/expected/extract__focus_ready.json +++ b/tests/fixtures/parity/expected/extract__focus_ready.json @@ -19,6 +19,27 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Beta fell 3% last year.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "Claim: Beta fell 3% last year.\n\nVerify it: lenz verify \"Beta fell 3% last year.\"\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Beta fell 3% last year.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Beta fell 3% last year.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Beta fell 3% last year.', identified_claims=[], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=None)" } diff --git a/tests/fixtures/parity/expected/extract__idempotency_replay.json b/tests/fixtures/parity/expected/extract__idempotency_replay.json index df61eab..91b0781 100644 --- a/tests/fixtures/parity/expected/extract__idempotency_replay.json +++ b/tests/fixtures/parity/expected/extract__idempotency_replay.json @@ -22,6 +22,30 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n 2. Beta fell 3% last year.\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Alpha rose 5% in 2024.', identified_claims=['Alpha rose 5% in 2024.', 'Beta fell 3% last year.'], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=None)" } diff --git a/tests/fixtures/parity/expected/extract__locate_all_dropped.json b/tests/fixtures/parity/expected/extract__locate_all_dropped.json index c015827..637c818 100644 --- a/tests/fixtures/parity/expected/extract__locate_all_dropped.json +++ b/tests/fixtures/parity/expected/extract__locate_all_dropped.json @@ -19,6 +19,27 @@ "presumed_intent": "Verify reported figures", "status": "not_a_claim" }, + "dump_unset": { + "candidate_claims": [], + "claim": "", + "domain": "", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [], + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "not_a_claim" + }, "render": "No verifiable claim found in that text.\n", - "render_json": "{\n \"status\": \"not_a_claim\",\n \"claim\": \"\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": []\n}\n" + "render_json": "{\n \"status\": \"not_a_claim\",\n \"claim\": \"\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": []\n}\n", + "repr": "ExtractedClaims(status='not_a_claim', claim='', identified_claims=[], candidate_claims=[], domain='', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=[])" } diff --git a/tests/fixtures/parity/expected/extract__locate_failed.json b/tests/fixtures/parity/expected/extract__locate_failed.json index df61eab..91b0781 100644 --- a/tests/fixtures/parity/expected/extract__locate_failed.json +++ b/tests/fixtures/parity/expected/extract__locate_failed.json @@ -22,6 +22,30 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n 2. Beta fell 3% last year.\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Alpha rose 5% in 2024.', identified_claims=['Alpha rose 5% in 2024.', 'Beta fell 3% last year.'], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=None)" } diff --git a/tests/fixtures/parity/expected/extract__locate_leading_whitespace.json b/tests/fixtures/parity/expected/extract__locate_leading_whitespace.json index a007c40..cac302f 100644 --- a/tests/fixtures/parity/expected/extract__locate_leading_whitespace.json +++ b/tests/fixtures/parity/expected/extract__locate_leading_whitespace.json @@ -30,6 +30,38 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "end": 25, + "start": 3, + "text": "Alpha rose 5% in 2024." + } + ] + } + ], + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "Claim: Alpha rose 5% in 2024.\n at 3-25: \"Alpha rose 5% in 2024.\"\n\nVerify it: lenz verify \"Alpha rose 5% in 2024.\"\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": 3,\n \"end\": 25,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n }\n ]\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": 3,\n \"end\": 25,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n }\n ]\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Alpha rose 5% in 2024.', identified_claims=[], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=[ClaimLocation(claim='Alpha rose 5% in 2024.', positions=[Position(start=3, end=25, text='Alpha rose 5% in 2024.')])])" } diff --git a/tests/fixtures/parity/expected/extract__locate_some_dropped.json b/tests/fixtures/parity/expected/extract__locate_some_dropped.json index 80d25e1..09f3760 100644 --- a/tests/fixtures/parity/expected/extract__locate_some_dropped.json +++ b/tests/fixtures/parity/expected/extract__locate_some_dropped.json @@ -43,6 +43,51 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "end": 22, + "start": 0, + "text": "Alpha rose 5% in 2024." + } + ] + }, + { + "claim": "Beta fell 3% last year.", + "positions": [ + { + "end": 46, + "start": 23, + "text": "Beta fell 3% last year." + } + ] + } + ], + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n at 0-22: \"Alpha rose 5% in 2024.\"\n 2. Beta fell 3% last year.\n at 23-46: \"Beta fell 3% last year.\"\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": 0,\n \"end\": 22,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n },\n {\n \"claim\": \"Beta fell 3% last year.\",\n \"positions\": [\n {\n \"start\": 23,\n \"end\": 46,\n \"text\": \"Beta fell 3% last year.\"\n }\n ]\n }\n ]\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": 0,\n \"end\": 22,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n },\n {\n \"claim\": \"Beta fell 3% last year.\",\n \"positions\": [\n {\n \"start\": 23,\n \"end\": 46,\n \"text\": \"Beta fell 3% last year.\"\n }\n ]\n }\n ]\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Alpha rose 5% in 2024.', identified_claims=['Alpha rose 5% in 2024.', 'Beta fell 3% last year.'], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=[ClaimLocation(claim='Alpha rose 5% in 2024.', positions=[Position(start=0, end=22, text='Alpha rose 5% in 2024.')]), ClaimLocation(claim='Beta fell 3% last year.', positions=[Position(start=23, end=46, text='Beta fell 3% last year.')])])" } diff --git a/tests/fixtures/parity/expected/extract__locate_true.json b/tests/fixtures/parity/expected/extract__locate_true.json index 80d25e1..09f3760 100644 --- a/tests/fixtures/parity/expected/extract__locate_true.json +++ b/tests/fixtures/parity/expected/extract__locate_true.json @@ -43,6 +43,51 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "end": 22, + "start": 0, + "text": "Alpha rose 5% in 2024." + } + ] + }, + { + "claim": "Beta fell 3% last year.", + "positions": [ + { + "end": 46, + "start": 23, + "text": "Beta fell 3% last year." + } + ] + } + ], + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n at 0-22: \"Alpha rose 5% in 2024.\"\n 2. Beta fell 3% last year.\n at 23-46: \"Beta fell 3% last year.\"\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": 0,\n \"end\": 22,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n },\n {\n \"claim\": \"Beta fell 3% last year.\",\n \"positions\": [\n {\n \"start\": 23,\n \"end\": 46,\n \"text\": \"Beta fell 3% last year.\"\n }\n ]\n }\n ]\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": 0,\n \"end\": 22,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n },\n {\n \"claim\": \"Beta fell 3% last year.\",\n \"positions\": [\n {\n \"start\": 23,\n \"end\": 46,\n \"text\": \"Beta fell 3% last year.\"\n }\n ]\n }\n ]\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Alpha rose 5% in 2024.', identified_claims=['Alpha rose 5% in 2024.', 'Beta fell 3% last year.'], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=[ClaimLocation(claim='Alpha rose 5% in 2024.', positions=[Position(start=0, end=22, text='Alpha rose 5% in 2024.')]), ClaimLocation(claim='Beta fell 3% last year.', positions=[Position(start=23, end=46, text='Beta fell 3% last year.')])])" } diff --git a/tests/fixtures/parity/expected/extract__no_match_with_focus.json b/tests/fixtures/parity/expected/extract__no_match_with_focus.json index 8097b22..16a3c71 100644 --- a/tests/fixtures/parity/expected/extract__no_match_with_focus.json +++ b/tests/fixtures/parity/expected/extract__no_match_with_focus.json @@ -10,6 +10,18 @@ "presumed_intent": "", "status": "no_match" }, + "dump_unset": { + "candidate_claims": [], + "claim": "", + "domain": "", + "identified_claims": [], + "key_entities": [], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "", + "status": "no_match" + }, "render": "No claims matched the focus.\n", - "render_json": "{\n \"status\": \"no_match\",\n \"claim\": \"\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"\",\n \"key_entities\": [],\n \"presumed_intent\": \"\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" + "render_json": "{\n \"status\": \"no_match\",\n \"claim\": \"\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"\",\n \"key_entities\": [],\n \"presumed_intent\": \"\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n", + "repr": "ExtractedClaims(status='no_match', claim='', identified_claims=[], candidate_claims=[], domain='', key_entities=[], presumed_intent='', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=None)" } diff --git a/tests/fixtures/parity/expected/extract__not_a_claim.json b/tests/fixtures/parity/expected/extract__not_a_claim.json index 1e7d7fe..48b365f 100644 --- a/tests/fixtures/parity/expected/extract__not_a_claim.json +++ b/tests/fixtures/parity/expected/extract__not_a_claim.json @@ -10,6 +10,18 @@ "presumed_intent": "", "status": "not_a_claim" }, + "dump_unset": { + "candidate_claims": [], + "claim": "", + "domain": "", + "identified_claims": [], + "key_entities": [], + "locations": null, + "original_input": "What a lovely day.", + "presumed_intent": "", + "status": "not_a_claim" + }, "render": "No verifiable claim found in that text.\n", - "render_json": "{\n \"status\": \"not_a_claim\",\n \"claim\": \"\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"\",\n \"key_entities\": [],\n \"presumed_intent\": \"\",\n \"original_input\": \"What a lovely day.\",\n \"locations\": null\n}\n" + "render_json": "{\n \"status\": \"not_a_claim\",\n \"claim\": \"\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"\",\n \"key_entities\": [],\n \"presumed_intent\": \"\",\n \"original_input\": \"What a lovely day.\",\n \"locations\": null\n}\n", + "repr": "ExtractedClaims(status='not_a_claim', claim='', identified_claims=[], candidate_claims=[], domain='', key_entities=[], presumed_intent='', original_input='What a lovely day.', locations=None)" } diff --git a/tests/fixtures/parity/expected/extract__not_a_claim_beside_claims.json b/tests/fixtures/parity/expected/extract__not_a_claim_beside_claims.json index 05550a3..13ec077 100644 --- a/tests/fixtures/parity/expected/extract__not_a_claim_beside_claims.json +++ b/tests/fixtures/parity/expected/extract__not_a_claim_beside_claims.json @@ -19,6 +19,27 @@ "presumed_intent": "Verify reported figures", "status": "not_a_claim" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024.", + "presumed_intent": "Verify reported figures", + "status": "not_a_claim" + }, "render": "Claim: Alpha rose 5% in 2024.\n\nVerify it: lenz verify \"Alpha rose 5% in 2024.\"\n", - "render_json": "{\n \"status\": \"not_a_claim\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024.\",\n \"locations\": null\n}\n" + "render_json": "{\n \"status\": \"not_a_claim\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024.\",\n \"locations\": null\n}\n", + "repr": "ExtractedClaims(status='not_a_claim', claim='Alpha rose 5% in 2024.', identified_claims=[], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024.', locations=None)" } diff --git a/tests/fixtures/parity/expected/extract__ready_one_claim.json b/tests/fixtures/parity/expected/extract__ready_one_claim.json index e65da3c..2f536e5 100644 --- a/tests/fixtures/parity/expected/extract__ready_one_claim.json +++ b/tests/fixtures/parity/expected/extract__ready_one_claim.json @@ -19,6 +19,27 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "Claim: Alpha rose 5% in 2024.\n\nVerify it: lenz verify \"Alpha rose 5% in 2024.\"\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024.\",\n \"locations\": null\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024.\",\n \"locations\": null\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Alpha rose 5% in 2024.', identified_claims=[], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024.', locations=None)" } diff --git a/tests/fixtures/parity/expected/extract__ready_several_claims.json b/tests/fixtures/parity/expected/extract__ready_several_claims.json index df61eab..91b0781 100644 --- a/tests/fixtures/parity/expected/extract__ready_several_claims.json +++ b/tests/fixtures/parity/expected/extract__ready_several_claims.json @@ -22,6 +22,30 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n 2. Beta fell 3% last year.\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Alpha rose 5% in 2024.', identified_claims=['Alpha rose 5% in 2024.', 'Beta fell 3% last year.'], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=None)" } diff --git a/tests/fixtures/parity/expected/extract__replay_later_200.json b/tests/fixtures/parity/expected/extract__replay_later_200.json index df61eab..91b0781 100644 --- a/tests/fixtures/parity/expected/extract__replay_later_200.json +++ b/tests/fixtures/parity/expected/extract__replay_later_200.json @@ -22,6 +22,30 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [ + "Alpha rose 5% in 2024.", + "Beta fell 3% last year." + ], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": null, + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "2 claims found:\n 1. Alpha rose 5% in 2024.\n 2. Beta fell 3% last year.\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [\n \"Alpha rose 5% in 2024.\",\n \"Beta fell 3% last year.\"\n ],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"Alpha rose 5% in 2024. Beta fell 3% last year.\",\n \"locations\": null\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Alpha rose 5% in 2024.', identified_claims=['Alpha rose 5% in 2024.', 'Beta fell 3% last year.'], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='Alpha rose 5% in 2024. Beta fell 3% last year.', locations=None)" } diff --git a/tests/fixtures/parity/expected/extract__url_input_located.json b/tests/fixtures/parity/expected/extract__url_input_located.json index ca78e96..d928040 100644 --- a/tests/fixtures/parity/expected/extract__url_input_located.json +++ b/tests/fixtures/parity/expected/extract__url_input_located.json @@ -30,6 +30,38 @@ "presumed_intent": "Verify reported figures", "status": "ready" }, + "dump_unset": { + "candidate_claims": [], + "claim": "Alpha rose 5% in 2024.", + "domain": "Economics", + "identified_claims": [], + "key_entities": [ + { + "name": "Alpha", + "type": "organization" + }, + { + "name": "Beta", + "type": "organization" + } + ], + "locations": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": [ + { + "end": null, + "start": null, + "text": "Alpha rose 5% in 2024." + } + ] + } + ], + "original_input": "https://example.com/an-article", + "presumed_intent": "Verify reported figures", + "status": "ready" + }, "render": "Claim: Alpha rose 5% in 2024.\n at \"Alpha rose 5% in 2024.\"\n\nVerify it: lenz verify \"Alpha rose 5% in 2024.\"\n", - "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"https://example.com/an-article\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": null,\n \"end\": null,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n }\n ]\n}\n" + "render_json": "{\n \"status\": \"ready\",\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"identified_claims\": [],\n \"candidate_claims\": [],\n \"domain\": \"Economics\",\n \"key_entities\": [\n {\n \"name\": \"Alpha\",\n \"type\": \"organization\"\n },\n {\n \"name\": \"Beta\",\n \"type\": \"organization\"\n }\n ],\n \"presumed_intent\": \"Verify reported figures\",\n \"original_input\": \"https://example.com/an-article\",\n \"locations\": [\n {\n \"claim\": \"Alpha rose 5% in 2024.\",\n \"positions\": [\n {\n \"start\": null,\n \"end\": null,\n \"text\": \"Alpha rose 5% in 2024.\"\n }\n ]\n }\n ]\n}\n", + "repr": "ExtractedClaims(status='ready', claim='Alpha rose 5% in 2024.', identified_claims=[], candidate_claims=[], domain='Economics', key_entities=[ExtractedEntity(name='Alpha', type='organization'), ExtractedEntity(name='Beta', type='organization')], presumed_intent='Verify reported figures', original_input='https://example.com/an-article', locations=[ClaimLocation(claim='Alpha rose 5% in 2024.', positions=[Position(start=None, end=None, text='Alpha rose 5% in 2024.')])])" } diff --git a/tests/fixtures/parity/expected/review__402_no_credits.json b/tests/fixtures/parity/expected/review__402_no_credits.json index 6f8a31c..d6661c5 100644 --- a/tests/fixtures/parity/expected/review__402_no_credits.json +++ b/tests/fixtures/parity/expected/review__402_no_credits.json @@ -20,7 +20,7 @@ "remaining": 100, "request_id": "", "requested": null, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/review__402_no_credits_exhausted.json b/tests/fixtures/parity/expected/review__402_no_credits_exhausted.json index c98a94a..75b0213 100644 --- a/tests/fixtures/parity/expected/review__402_no_credits_exhausted.json +++ b/tests/fixtures/parity/expected/review__402_no_credits_exhausted.json @@ -20,7 +20,7 @@ "remaining": 0, "request_id": "", "requested": null, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/review__422_webhook_secret_missing.json b/tests/fixtures/parity/expected/review__422_webhook_secret_missing.json new file mode 100644 index 0000000..aa5a20c --- /dev/null +++ b/tests/fixtures/parity/expected/review__422_webhook_secret_missing.json @@ -0,0 +1,30 @@ +{ + "error": { + "cause": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "code": "webhook_secret_missing", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [ + { + "loc": [ + "body", + "webhook_url" + ], + "msg": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend." + } + ], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.\n Fix: Check the request body against the OpenAPI spec.", + "message": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/review__delete_not_a_route.json b/tests/fixtures/parity/expected/review__delete_not_a_route.json new file mode 100644 index 0000000..ebe2cee --- /dev/null +++ b/tests/fixtures/parity/expected/review__delete_not_a_route.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "HTTP 405", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "HTTP 405\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 405", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "HTTP 405", + "status": 405 + } + }, + "request_id": "", + "status_code": 405, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/review__get_assessing.json b/tests/fixtures/parity/expected/review__get_assessing.json index 72f3ffc..d03a95c 100644 --- a/tests/fixtures/parity/expected/review__get_assessing.json +++ b/tests/fixtures/parity/expected/review__get_assessing.json @@ -49,8 +49,110 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 10, + "review_id": "d6b2bd72", + "status": "assessing", + "summary": { + "assessments": { + "completed": 0, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "running", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": null, + "index": 0, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "running", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": null, + "index": 1, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -104,5 +206,6 @@ "exit_code": 2, "render": "Review d6b2bd72: assessing — 2 claims: 2 not checked · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n ? · quick check\n\n[2/2] Water boils at 100 C at sea level.\n ? · quick check\n", "render_issues": "Review d6b2bd72: assessing — 2 claims: 2 not checked · 2 credits charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='assessing', outcome=None, created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=0, failed=0), verifications=None, issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=10, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=None, assessment=ReviewAssessment(status='running', verdict=None, confidence=None, rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=None, verification=None, suggested_edits=None), ReviewClaim(index=1, claim='Water boils at 100 C at sea level.', positions=None, result=None, assessment=ReviewAssessment(status='running', verdict=None, confidence=None, rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=None, verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_assessment_rows_full_fields.json b/tests/fixtures/parity/expected/review__get_assessment_rows_full_fields.json index 8322f9e..04bcf67 100644 --- a/tests/fixtures/parity/expected/review__get_assessment_rows_full_fields.json +++ b/tests/fixtures/parity/expected/review__get_assessment_rows_full_fields.json @@ -68,8 +68,170 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 1, + "failure": { + "docs_url": null, + "failure_class": "upstream_unavailable", + "failure_reason": "timeout", + "hint": "The check ran out of time. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + } + ], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "cap", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": "Two reviewers found the date wrong.", + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": "The EU AI Act entered into force in August 2024.", + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": null, + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 0, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 1 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": "One reviewer read the claim as about the entry into force.", + "error_code": null, + "failure": null, + "hint": "This text holds more than one claim.", + "identified_claims": [ + "The EU AI Act took effect in 2024.", + "It took effect in March." + ], + "rationale": "Two reviewers found the date wrong.", + "status": "completed", + "suggested_rewrite": "The EU AI Act entered into force in August 2024.", + "verdict": "False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "cap", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": "timeout", + "failure": { + "docs_url": null, + "failure_class": "upstream_unavailable", + "failure_reason": "timeout", + "hint": "The check ran out of time. Retry it; nothing was charged.", + "retryable": true + }, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "failed", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": null, + "index": 1, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -164,5 +326,6 @@ "exit_code": 2, "render": "Review d6b2bd72: incomplete — 2 claims: 1 issue, 1 failed · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n Two reviewers found the date wrong.\n Not deep-checked (cap).\n\n[2/2] The Eiffel Tower is in Berlin.\n Quick check failed: The check ran out of time. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", "render_issues": "Review d6b2bd72: incomplete — 2 claims: 1 issue, 1 failed · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n Two reviewers found the date wrong.\n Suggested rewrite: The EU AI Act entered into force in August 2024.\n Not deep-checked (cap).\n\n[2/2] The Eiffel Tower is in Berlin.\n Assessment failed: The check ran out of time. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='incomplete', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=0, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=1), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=1, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[ReviewIssue(claim_index=0, claim='The EU AI Act took effect in March 2024.', verified_claim=None, verdict='False', confidence='high', source='assessment', verification_id=None, verification_status=None, verification_url=None, url=None, escalation=Escalation(matched_rules=['verdict'], disposition='cap'), key_finding=None, rationale='Two reviewers found the date wrong.', suggested_rewrite='The EU AI Act entered into force in August 2024.', failure=None, suggested_edits=None)], failures=[ReviewFailure(claim_index=1, claim='The Eiffel Tower is in Berlin.', stage='assessment', failure=FailureBlock(failure_reason='timeout', failure_class='upstream_unavailable', retryable=True, hint='The check ran out of time. Retry it; nothing was charged.', docs_url=None))], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='assessment', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale='Two reviewers found the date wrong.', dissent='One reviewer read the claim as about the entry into force.', verification_url=None, error_code=None, identified_claims=['The EU AI Act took effect in 2024.', 'It took effect in March.'], hint='This text holds more than one claim.', suggested_rewrite='The EU AI Act entered into force in August 2024.', failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='cap'), verification=None, suggested_edits=None), ReviewClaim(index=1, claim='The Eiffel Tower is in Berlin.', positions=None, result=None, assessment=ReviewAssessment(status='failed', verdict=None, confidence=None, rationale=None, dissent=None, verification_url=None, error_code='timeout', identified_claims=[], hint=None, suggested_rewrite=None, failure=FailureBlock(failure_reason='timeout', failure_class='upstream_unavailable', retryable=True, hint='The check ran out of time. Retry it; nothing was charged.', docs_url=None)), escalation=None, verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_clean.json b/tests/fixtures/parity/expected/review__get_citations_clean.json index b9ecbce..403f0e1 100644 --- a/tests/fixtures/parity/expected/review__get_citations_clean.json +++ b/tests/fixtures/parity/expected/review__get_citations_clean.json @@ -114,8 +114,183 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 3 }, @@ -177,5 +352,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 checked.\n", "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=3), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=Position(start=111, end=178, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_failed_and_unchecked.json b/tests/fixtures/parity/expected/review__get_citations_failed_and_unchecked.json index 81f36b2..b12ff7d 100644 --- a/tests/fixtures/parity/expected/review__get_citations_failed_and_unchecked.json +++ b/tests/fixtures/parity/expected/review__get_citations_failed_and_unchecked.json @@ -130,8 +130,199 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 0, + "failed": 1, + "unchecked": 1 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#internal", + "failure_class": "internal", + "failure_reason": "internal", + "hint": "The check failed on our side. Try again later.", + "retryable": false + }, + "reference": "a report from the ministry" + } + ], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#internal", + "failure_class": "internal", + "failure_reason": "internal", + "hint": "The check failed on our side. Try again later.", + "retryable": false + }, + "hint": "The check failed on our side. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": null, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": null, + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "unchecked", + "unchecked_reason": "no_text" + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "unchecked", + "is_issue": false, + "source": null + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -193,5 +384,6 @@ "exit_code": 2, "render": "Review d6b2bd72: incomplete — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 could not be checked.\n\n[source 1/2] Could not be checked this time.\n https://example.gov/report-2024\n The check failed on our side. Try again later.\n\nCheck these by hand (Lenz could not check them)\n[source 2/2] The page gave no text to read.\n https://example.org/statement\n", "render_issues": "Review d6b2bd72: incomplete — 1 claim: 1 true or mostly true · 1 credit charged\n\n2 sources cited in your draft\n2 could not be checked.\n\n[source 1/2] Could not be checked this time.\n https://example.gov/report-2024\n The check failed on our side. Try again later.\n\nCheck these by hand (Lenz could not check them)\n[source 2/2] The page gave no text to read.\n https://example.org/statement\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='incomplete', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=0, unchecked=1, failed=1), citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=1), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[ReviewCitationFailure(citation_index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, failure=FailureBlock(failure_reason='internal', failure_class='internal', retryable=False, hint='The check failed on our side. Try again later.', docs_url='https://lenz.io/docs/errors#internal'))], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=None, check=ReviewCitationCheck(status='failed', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint='The check failed on our side. Try again later.', failure=FailureBlock(failure_reason='internal', failure_class='internal', retryable=False, hint='The check failed on our side. Try again later.', docs_url='https://lenz.io/docs/errors#internal'))), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=Position(start=111, end=178, text=None), result=ReviewCitationResult(finding='unchecked', source=None, is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='unchecked', snippet=None, rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason='no_text', hint=None, failure=None))])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_issue_full.json b/tests/fixtures/parity/expected/review__get_citations_issue_full.json index cbb418a..558e623 100644 --- a/tests/fixtures/parity/expected/review__get_citations_issue_full.json +++ b/tests/fixtures/parity/expected/review__get_citations_issue_full.json @@ -136,8 +136,205 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 1, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": null, + "finding": "contradicted", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "rationale": "The page states it.", + "reference": "a report from the ministry", + "snippet": "The source says so.", + "source": "support", + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "contradicted", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "contradicted", + "is_issue": true, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 3 }, @@ -199,5 +396,6 @@ "exit_code": 1, "render": "Review d6b2bd72: issues found — 1 claim: 1 true or mostly true · 3 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 true or mostly true · 3 credits charged\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='issues_found', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=1, citations_skipped=None), credits=ReviewCredits(charged=3), poll_after_seconds=None, issues=[], failures=[], citation_issues=[ReviewCitationIssue(citation_index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), finding='contradicted', source='support', snippet='The source says so.', rationale='The page states it.', missing_quote=None, metadata_differences=[], page_title='Page at https://example.gov/report-2024', failure=None)], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='contradicted', source='support', is_issue=True), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='contradicted', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=Position(start=111, end=178, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_issue_issues_view.json b/tests/fixtures/parity/expected/review__get_citations_issue_issues_view.json index 59abdb7..99f5e5f 100644 --- a/tests/fixtures/parity/expected/review__get_citations_issue_issues_view.json +++ b/tests/fixtures/parity/expected/review__get_citations_issue_issues_view.json @@ -26,8 +26,93 @@ ], "citations": [], "claims": [], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 1, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "issues" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": null, + "finding": "contradicted", + "metadata_differences": [], + "missing_quote": null, + "page_title": "Page at https://example.gov/report-2024", + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "rationale": "The page states it.", + "reference": "a report from the ministry", + "snippet": "The source says so.", + "source": "support", + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 3 }, @@ -89,5 +174,6 @@ "exit_code": 1, "render": "Review d6b2bd72: issues found — 1 claim: 1 not checked · 3 credits charged\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 not checked · 3 credits charged\n\n2 sources cited in your draft\n2 checked, 1 with a problem.\n\n[source 1/2] Contradicted\n Unemployment fell to 4.1% in 2024, according to a report from the ministry.\n https://example.gov/report-2024\n The source says:\n “The source says so.”\n Reviewer's note: The page states it.\n\nA reviewer's note is reasoning, not a checked source; the quoted passage is from the page.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='issues', status='completed', outcome='issues_found', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=1, citations_skipped=None), credits=ReviewCredits(charged=3), poll_after_seconds=None, issues=[], failures=[], citation_issues=[ReviewCitationIssue(citation_index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), finding='contradicted', source='support', snippet='The source says so.', rationale='The page states it.', missing_quote=None, metadata_differences=[], page_title='Page at https://example.gov/report-2024', failure=None)], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_limit_exactly_at_limit.json b/tests/fixtures/parity/expected/review__get_citations_limit_exactly_at_limit.json index 21955f5..d8c2878 100644 --- a/tests/fixtures/parity/expected/review__get_citations_limit_exactly_at_limit.json +++ b/tests/fixtures/parity/expected/review__get_citations_limit_exactly_at_limit.json @@ -114,8 +114,183 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 2, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 3 }, @@ -177,5 +352,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 checked.\n", "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=2, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=2, citation_limit=2, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=3), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=Position(start=111, end=178, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_limit_reached.json b/tests/fixtures/parity/expected/review__get_citations_limit_reached.json index 5e3bb88..8a1c54a 100644 --- a/tests/fixtures/parity/expected/review__get_citations_limit_reached.json +++ b/tests/fixtures/parity/expected/review__get_citations_limit_reached.json @@ -75,8 +75,157 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [ + { + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "reference": "a statement", + "sentence": "The agency said so in a statement." + } + ], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 1, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 1, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 1, + "citation_limit_reached": true, + "citations_found": 2, + "citations_selected": 1, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -151,5 +300,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 2 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n1 checked.\n1 more was not checked: one check covers 1.\n\n1 more citation was found but not checked.\n", "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 2 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n1 checked.\n1 more was not checked: one check covers 1.\n\n1 more citation was found but not checked.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=1, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=1, citation_limit=1, citation_limit_reached=True, citation_checks=ReviewCitationCheckCounts(checked=1, unchecked=0, failed=0), citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[ReviewMoreCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, sentence='The agency said so in a statement.', position=Position(start=111, end=178, text=None))], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_no_claim_but_citations.json b/tests/fixtures/parity/expected/review__get_citations_no_claim_but_citations.json index 8093baf..d2bf7e8 100644 --- a/tests/fixtures/parity/expected/review__get_citations_no_claim_but_citations.json +++ b/tests/fixtures/parity/expected/review__get_citations_no_claim_but_citations.json @@ -83,8 +83,152 @@ } ], "claims": [], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 0, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 0, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -146,5 +290,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — no claims checked · 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n", "render_issues": "Review d6b2bd72: clean — no claims checked · 2 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=0, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=0, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[], citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=Position(start=111, end=178, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_no_claim_no_citation.json b/tests/fixtures/parity/expected/review__get_citations_no_claim_no_citation.json index c088cac..74da78d 100644 --- a/tests/fixtures/parity/expected/review__get_citations_no_claim_no_citation.json +++ b/tests/fixtures/parity/expected/review__get_citations_no_claim_no_citation.json @@ -4,8 +4,68 @@ "citation_issues": [], "citations": [], "claims": [], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_claim", + "hint": "No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.", + "retryable": false + }, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": "unchecked", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -62,5 +122,6 @@ "exit_code": 2, "render": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run\nthe text through /extract to enumerate its claims.\n", "render_issues": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run\nthe text through /extract to enumerate its claims.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='failed', outcome='unchecked', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=None, claim_limit=20, claim_limit_reached=None, input_truncated=False, assessments=None, verifications=None, issues=0, citations_found=None, citations_selected=None, citation_limit=20, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=0), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=None, more_claim_locations=None, more_citations=None, failure=FailureBlock(failure_reason='no_claim', failure_class='invalid_input', retryable=False, hint='No factual statement that can be checked against evidence was found in the input. Send one factual claim, or run the text through /extract to enumerate its claims.', docs_url='https://lenz.io/docs/errors#invalid-input'), claims=[], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_not_asked.json b/tests/fixtures/parity/expected/review__get_citations_not_asked.json index a7c1d4a..b0e08d1 100644 --- a/tests/fixtures/parity/expected/review__get_citations_not_asked.json +++ b/tests/fixtures/parity/expected/review__get_citations_not_asked.json @@ -35,8 +35,100 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -94,5 +186,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=1), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_only.json b/tests/fixtures/parity/expected/review__get_citations_only.json index ec2968f..7b79f80 100644 --- a/tests/fixtures/parity/expected/review__get_citations_only.json +++ b/tests/fixtures/parity/expected/review__get_citations_only.json @@ -83,8 +83,159 @@ } ], "claims": [], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "positions": null + } + ], + "more_claims": [ + "The EU AI Act took effect in March 2024." + ], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 0, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 0, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 0, + "claim_limit_reached": true, + "claims_selected": 0, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -153,5 +304,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — no claims checked · 2 credits charged\n\n2 sources cited in your draft\n2 checked.\n\n1 more claim was found but not checked.\n", "render_issues": "Review d6b2bd72: clean — no claims checked · 2 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n\n1 more claim was found but not checked.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=0, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=0, claim_limit=0, claim_limit_reached=True, input_truncated=False, assessments=ReviewAssessmentCounts(completed=0, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=['The EU AI Act took effect in March 2024.'], more_claim_locations=[ClaimLocation(claim='The EU AI Act took effect in March 2024.', positions=None)], more_citations=[], failure=None, claims=[], citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=Position(start=111, end=178, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_partly_supported.json b/tests/fixtures/parity/expected/review__get_citations_partly_supported.json index 1f7c6fc..144016b 100644 --- a/tests/fixtures/parity/expected/review__get_citations_partly_supported.json +++ b/tests/fixtures/parity/expected/review__get_citations_partly_supported.json @@ -114,8 +114,183 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 3 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 2, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "status": "completed", + "support": "partly_supported", + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": { + "end": 110, + "start": 0, + "text": null + }, + "quotes": [], + "reference": "a report from the ministry", + "result": { + "finding": "partly_supported", + "is_issue": false, + "source": "support" + }, + "statement": "Unemployment fell to 4.1% in 2024, according to a report from the ministry." + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": "en", + "page_published_date": null, + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "quote": null, + "rationale": "The page states it.", + "registered": null, + "snippet": "The source says so.", + "source_url": "https://example.org/statement", + "source_version": null, + "status": "completed", + "support": "supported", + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": { + "end": 178, + "start": 111, + "text": null + }, + "quotes": [], + "reference": "a statement", + "result": { + "finding": "supported", + "is_issue": false, + "source": "support" + }, + "statement": "The agency said so in a statement." + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 3 }, @@ -177,5 +352,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\n2 checked.\n", "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 3 credits charged\n\nNo issues.\n\n2 sources cited in your draft\n2 checked.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=2, unchecked=0, failed=0), citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=3), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024, according to a report from the ministry.', quotes=[], position=Position(start=0, end=110, text=None), result=ReviewCitationResult(finding='partly_supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.gov/report-2024', page_published_date=None, page_language='en', source_url='https://example.gov/report-2024', source_version=None, support='partly_supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=Position(start=111, end=178, text=None), result=ReviewCitationResult(finding='supported', source='support', is_issue=False), check=ReviewCitationCheck(status='completed', page_read='full', page_title='Page at https://example.org/statement', page_published_date=None, page_language='en', source_url='https://example.org/statement', source_version=None, support='supported', snippet='The source says so.', rationale='The page states it.', quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_running.json b/tests/fixtures/parity/expected/review__get_citations_running.json index 197c7ad..acc4c84 100644 --- a/tests/fixtures/parity/expected/review__get_citations_running.json +++ b/tests/fixtures/parity/expected/review__get_citations_running.json @@ -99,7 +99,166 @@ } ], "completed_at": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 15, + "review_id": "d6b2bd72", + "status": "verifying", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": { + "checked": 0, + "failed": 0, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "a report from the ministry", + "result": null, + "statement": null + }, + { + "check": { + "doi_registered": null, + "failure": null, + "hint": null, + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "pending", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "a statement", + "result": null, + "statement": null + } + ], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": null, + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -161,5 +320,6 @@ "exit_code": 2, "render": "Review d6b2bd72: verifying — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n2 sources cited in your draft\nLenz checks each one: does the source say what your draft says it does.\n", "render_issues": "Review d6b2bd72: verifying — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n\n2 sources cited in your draft\nLenz checks each one: does the source say what your draft says it does.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='verifying', outcome=None, created_at='2026-10-01T09:07:00.123456Z', completed_at=None, language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=2, citations_selected=2, citation_limit=20, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=0, unchecked=0, failed=0), citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=1), poll_after_seconds=15, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[ReviewCitation(index=0, reference='a report from the ministry', cited_url='https://example.gov/report-2024', doi=None, statement=None, quotes=[], position=None, result=None, check=ReviewCitationCheck(status='pending', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None)), ReviewCitation(index=1, reference='a statement', cited_url='https://example.org/statement', doi=None, statement=None, quotes=[], position=None, result=None, check=ReviewCitationCheck(status='pending', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint=None, failure=None))])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_skipped_switched_off.json b/tests/fixtures/parity/expected/review__get_citations_skipped_switched_off.json index 9656e4e..bac0fa1 100644 --- a/tests/fixtures/parity/expected/review__get_citations_skipped_switched_off.json +++ b/tests/fixtures/parity/expected/review__get_citations_skipped_switched_off.json @@ -35,8 +35,100 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": "switched_off", + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -94,5 +186,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=20, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped='switched_off'), credits=ReviewCredits(charged=1), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_citations_skipped_url_input.json b/tests/fixtures/parity/expected/review__get_citations_skipped_url_input.json index 4a4d436..f74fcd2 100644 --- a/tests/fixtures/parity/expected/review__get_citations_skipped_url_input.json +++ b/tests/fixtures/parity/expected/review__get_citations_skipped_url_input.json @@ -4,8 +4,68 @@ "citation_issues": [], "citations": [], "claims": [], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": null, + "retryable": true + }, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": "unchecked", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 20, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": 20, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": "url_input", + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -62,5 +122,6 @@ "exit_code": 2, "render": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: upstream_unavailable\n\nSources on a linked page are not checked. Paste the page's text instead to check them.\n", "render_issues": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: upstream_unavailable\n\nSources on a linked page are not checked. Paste the page's text instead to check them.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='failed', outcome='unchecked', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=20, suggest_edits=False), summary=ReviewSummary(claims_selected=None, claim_limit=20, claim_limit_reached=None, input_truncated=False, assessments=None, verifications=None, issues=0, citations_found=None, citations_selected=None, citation_limit=20, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped='url_input'), credits=ReviewCredits(charged=0), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=None, more_claim_locations=None, more_citations=None, failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint=None, docs_url='https://lenz.io/docs/errors#upstream-unavailable'), claims=[], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_claim_limit_not_reached.json b/tests/fixtures/parity/expected/review__get_claim_limit_not_reached.json index fe88fdc..b544e5b 100644 --- a/tests/fixtures/parity/expected/review__get_claim_limit_not_reached.json +++ b/tests/fixtures/parity/expected/review__get_claim_limit_not_reached.json @@ -65,8 +65,130 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 3, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 3, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -124,5 +246,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level.\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=3, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=3, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None), ReviewClaim(index=1, claim='Water boils at 100 C at sea level.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_claim_limit_reached_exact.json b/tests/fixtures/parity/expected/review__get_claim_limit_reached_exact.json index 50f547c..d282376 100644 --- a/tests/fixtures/parity/expected/review__get_claim_limit_reached_exact.json +++ b/tests/fixtures/parity/expected/review__get_claim_limit_reached_exact.json @@ -65,8 +65,130 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 2, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 2, + "claim_limit_reached": true, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -124,5 +246,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level.\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=2, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=2, claim_limit_reached=True, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None), ReviewClaim(index=1, claim='Water boils at 100 C at sea level.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_completed_assess_only.json b/tests/fixtures/parity/expected/review__get_completed_assess_only.json index 382a665..ffc8ab2 100644 --- a/tests/fixtures/parity/expected/review__get_completed_assess_only.json +++ b/tests/fixtures/parity/expected/review__get_completed_assess_only.json @@ -67,8 +67,156 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 1, + "confidence": "high", + "escalation": { + "disposition": "cap", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": null, + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 0, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "cap", + "matched_rules": [ + "verdict" + ] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -150,5 +298,6 @@ "exit_code": 1, "render": "Review d6b2bd72: issues found — 2 claims: 1 issue, 1 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n[2/2] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Not deep-checked (cap).\n", "render_issues": "Review d6b2bd72: issues found — 2 claims: 1 issue, 1 true or mostly true · 2 credits charged\n\n[2/2] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Not deep-checked (cap).\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='issues_found', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=0, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=1, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[ReviewIssue(claim_index=1, claim='The Eiffel Tower is in Berlin.', verified_claim=None, verdict='False', confidence='high', source='assessment', verification_id=None, verification_status=None, verification_url=None, url=None, escalation=Escalation(matched_rules=['verdict'], disposition='cap'), key_finding=None, rationale=None, suggested_rewrite=None, failure=None, suggested_edits=None)], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None), ReviewClaim(index=1, claim='The Eiffel Tower is in Berlin.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='assessment', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='cap'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_completed_clean.json b/tests/fixtures/parity/expected/review__get_completed_clean.json index b028662..9ff4a6b 100644 --- a/tests/fixtures/parity/expected/review__get_completed_clean.json +++ b/tests/fixtures/parity/expected/review__get_completed_clean.json @@ -65,8 +65,130 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Paris is the capital of France.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -124,5 +246,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n True (high) · quick check\n\n[2/2] Paris is the capital of France.\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None), ReviewClaim(index=1, claim='Paris is the capital of France.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_completed_issues_all_verified.json b/tests/fixtures/parity/expected/review__get_completed_issues_all_verified.json index aa86f83..80599b7 100644 --- a/tests/fixtures/parity/expected/review__get_completed_issues_all_verified.json +++ b/tests/fixtures/parity/expected/review__get_completed_issues_all_verified.json @@ -38,7 +38,7 @@ "claim": "The EU AI Act took effect in March 2024.", "confidence": "high", "content_status": "available", - "created_at": "2026-10-01T09:49:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "depth": "standard", "domain": "", "entities": [], @@ -50,11 +50,11 @@ "modified_at": null, "status": "completed", "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", - "task_id": "c010dca9a43543f518e49f4ec7842ceb", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "b4e2e99d", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "visibility": "private", "warnings": [ "w1" @@ -95,7 +95,7 @@ "claim": "The Eiffel Tower is in Berlin.", "confidence": "high", "content_status": "available", - "created_at": "2026-10-01T09:49:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "depth": "standard", "domain": "", "entities": [], @@ -107,11 +107,11 @@ "modified_at": null, "status": "completed", "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "task_id": "4634b89821fd877f1edf8aaeb82b7ee5", - "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "url": "https://lenz.io/c/the-earth-is-round-4d44dfaf", "verdict": "False", - "verification_id": "b79019e4", - "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", + "verification_id": "4d44dfaf", + "verification_url": "https://lenz.io/api/v1/verifications/4d44dfaf", "visibility": "private", "warnings": [ "w1" @@ -149,8 +149,8 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 23 }, @@ -173,11 +173,11 @@ "source": "verification", "suggested_edits": null, "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "verified_claim": null }, { @@ -196,11 +196,264 @@ "source": "verification", "suggested_edits": null, "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "url": "https://lenz.io/c/the-earth-is-round-4d44dfaf", "verdict": "False", - "verification_id": "b79019e4", + "verification_id": "4d44dfaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", + "verification_url": "https://lenz.io/api/v1/verifications/4d44dfaf", + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 3, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 3, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 2, + "failed": 0, + "planned": 2 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "medium", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "Mostly False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The EU AI Act took effect in March 2024.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The EU AI Act took effect in March 2024.", + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "visibility": "private", + "warnings": [ + "w1" + ] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "url": "https://lenz.io/c/the-earth-is-round-4d44dfaf", + "verdict": "False", + "verification_id": "4d44dfaf", + "verification_url": "https://lenz.io/api/v1/verifications/4d44dfaf", + "visibility": "private", + "warnings": [ + "w1" + ] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Paris is the capital of France.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 2, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 23 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "verified_claim": null + }, + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 1, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-4d44dfaf", + "verdict": "False", + "verification_id": "4d44dfaf", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/4d44dfaf", "verified_claim": null } ], @@ -253,7 +506,8 @@ "view": "full" }, "exit_code": 1, - "render": "Review d6b2bd72: issues found — 3 claims: 2 issues, 1 true or mostly true · 2 of 2 deep-checked · 23 credits charged\n\n[1/3] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[2/3] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b79019e4\n\n[3/3] Paris is the capital of France.\n True (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_issues": "Review d6b2bd72: issues found — 3 claims: 2 issues, 1 true or mostly true · 2 of 2 deep-checked · 23 credits charged\n\n[1/3] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[2/3] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b79019e4\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_json": null + "render": "Review d6b2bd72: issues found — 3 claims: 2 issues, 1 true or mostly true · 2 of 2 deep-checked · 23 credits charged\n\n[1/3] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\n[2/3] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-4d44dfaf\n\n[3/3] Paris is the capital of France.\n True (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: issues found — 3 claims: 2 issues, 1 true or mostly true · 2 of 2 deep-checked · 23 credits charged\n\n[1/3] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\n[2/3] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-4d44dfaf\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='issues_found', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=3, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=3, failed=0), verifications=ReviewVerificationCounts(planned=2, completed=2, failed=0), issues=2, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=23), poll_after_seconds=None, issues=[ReviewIssue(claim_index=0, claim='The EU AI Act took effect in March 2024.', verified_claim=None, verdict='False', confidence='high', source='verification', verification_id='68ca4aaf', verification_status='completed', verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding='Finding for The EU AI Act took effect in March 2024.', rationale=None, suggested_rewrite='Fixed: The EU AI Act took effect in March 2024.', failure=None, suggested_edits=None), ReviewIssue(claim_index=1, claim='The Eiffel Tower is in Berlin.', verified_claim=None, verdict='False', confidence='high', source='verification', verification_id='4d44dfaf', verification_status='completed', verification_url='https://lenz.io/api/v1/verifications/4d44dfaf', url='https://lenz.io/c/the-earth-is-round-4d44dfaf', escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding='Finding for The Eiffel Tower is in Berlin.', rationale=None, suggested_rewrite='Fixed: The Eiffel Tower is in Berlin.', failure=None, suggested_edits=None)], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='verification', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='Mostly False', confidence='medium', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='completed', content_status='available', verification_id='68ca4aaf', task_id='7cff18da2d979d6370e54a160be121ad', claim='The EU AI Act took effect in March 2024.', language='en', visibility='private', depth='standard', domain='', entities=[], verdict='False', confidence='high', lenz_score=2, key_finding='Finding for The EU AI Act took effect in March 2024.', executive_summary='Summary for The EU AI Act took effect in March 2024.', suggested_rewrite='Fixed: The EU AI Act took effect in March 2024.', warnings=['w1'], created_at='2026-10-01T09:14:00.123456+00:00', modified_at=None, verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', failure=None), suggested_edits=None), ReviewClaim(index=1, claim='The Eiffel Tower is in Berlin.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='verification', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='completed', content_status='available', verification_id='4d44dfaf', task_id='0b92b443eb69b9c71434fdc64f2ca8be', claim='The Eiffel Tower is in Berlin.', language='en', visibility='private', depth='standard', domain='', entities=[], verdict='False', confidence='high', lenz_score=2, key_finding='Finding for The Eiffel Tower is in Berlin.', executive_summary='Summary for The Eiffel Tower is in Berlin.', suggested_rewrite='Fixed: The Eiffel Tower is in Berlin.', warnings=['w1'], created_at='2026-10-01T09:14:00.123456+00:00', modified_at=None, verification_url='https://lenz.io/api/v1/verifications/4d44dfaf', url='https://lenz.io/c/the-earth-is-round-4d44dfaf', failure=None), suggested_edits=None), ReviewClaim(index=2, claim='Paris is the capital of France.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_completed_issues_full.json b/tests/fixtures/parity/expected/review__get_completed_issues_full.json index b2bfc03..58214d0 100644 --- a/tests/fixtures/parity/expected/review__get_completed_issues_full.json +++ b/tests/fixtures/parity/expected/review__get_completed_issues_full.json @@ -38,7 +38,7 @@ "claim": "The EU AI Act took effect in March 2024.", "confidence": "high", "content_status": "available", - "created_at": "2026-10-01T09:42:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "depth": "standard", "domain": "", "entities": [], @@ -50,11 +50,11 @@ "modified_at": null, "status": "completed", "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", - "task_id": "7df4009bc27aab64ce5d5a01fbafe599", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "b4e2e99d", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "visibility": "private", "warnings": [ "w1" @@ -141,7 +141,7 @@ "modified_at": null, "status": "failed", "suggested_rewrite": null, - "task_id": "5ccac77f53ff49328ec134fb29d66542", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", "url": null, "verdict": null, "verification_id": null, @@ -181,8 +181,8 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 13 }, @@ -218,11 +218,315 @@ "source": "verification", "suggested_edits": null, "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "verified_claim": null + }, + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 2, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "hint": null, + "retryable": false + }, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 3, + "failed": 1 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 4, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 1, + "failed": 1, + "planned": 2 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "medium", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "Mostly False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The EU AI Act took effect in March 2024.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The EU AI Act took effect in March 2024.", + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "visibility": "private", + "warnings": [ + "w1" + ] + } + }, + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": "upstream_unavailable", + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "failed", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": null, + "index": 1, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 2, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": null, + "confidence": null, + "content_status": "available", + "created_at": null, + "depth": null, + "domain": null, + "entities": [], + "executive_summary": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "hint": null, + "retryable": false + }, + "key_finding": null, + "language": null, + "lenz_score": null, + "modified_at": null, + "status": "failed", + "suggested_rewrite": null, + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "url": null, + "verdict": null, + "verification_id": null, + "verification_url": null, + "visibility": null, + "warnings": [] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Paris is the capital of France.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 3, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 13 + }, + "failure": null, + "failures": [ + { + "claim": "Water boils at 100 C at sea level.", + "claim_index": 1, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + } + ], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "verified_claim": null }, { @@ -304,7 +608,8 @@ "view": "full" }, "exit_code": 2, - "render": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 true or mostly true, 1 failed · 1 of 2 deep-checked · 13 credits \ncharged\n\n[1/4] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[2/4] Water boils at 100 C at sea level.\n Quick check failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\n[3/4] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Deep check failed: The deep check failed.\n\n[4/4] Paris is the capital of France.\n True (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_issues": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 true or mostly true, 1 failed · 1 of 2 deep-checked · 13 credits \ncharged\n\n[1/4] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[3/4] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Deep check failed: research_empty\n\n[2/4] Water boils at 100 C at sea level.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_json": null + "render": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 true or mostly true, 1 failed · 1 of 2 deep-checked · 13 credits \ncharged\n\n[1/4] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\n[2/4] Water boils at 100 C at sea level.\n Quick check failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\n[3/4] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Deep check failed: The deep check failed.\n\n[4/4] Paris is the capital of France.\n True (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 true or mostly true, 1 failed · 1 of 2 deep-checked · 13 credits \ncharged\n\n[1/4] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\n[3/4] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Deep check failed: research_empty\n\n[2/4] Water boils at 100 C at sea level.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='incomplete', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=4, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=3, failed=1), verifications=ReviewVerificationCounts(planned=2, completed=1, failed=1), issues=2, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=13), poll_after_seconds=None, issues=[ReviewIssue(claim_index=0, claim='The EU AI Act took effect in March 2024.', verified_claim=None, verdict='False', confidence='high', source='verification', verification_id='68ca4aaf', verification_status='completed', verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding='Finding for The EU AI Act took effect in March 2024.', rationale=None, suggested_rewrite='Fixed: The EU AI Act took effect in March 2024.', failure=None, suggested_edits=None), ReviewIssue(claim_index=2, claim='The Eiffel Tower is in Berlin.', verified_claim=None, verdict='False', confidence='high', source='assessment', verification_id=None, verification_status='failed', verification_url=None, url=None, escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding=None, rationale=None, suggested_rewrite=None, failure=FailureBlock(failure_reason='research_empty', failure_class='insufficient_evidence', retryable=False, hint=None, docs_url='https://lenz.io/docs/errors#insufficient-evidence'), suggested_edits=None)], failures=[ReviewFailure(claim_index=1, claim='Water boils at 100 C at sea level.', stage='assessment', failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='A model provider was unavailable for this item. Retry it; nothing was charged.', docs_url='https://lenz.io/docs/errors#upstream-unavailable'))], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='verification', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='Mostly False', confidence='medium', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='completed', content_status='available', verification_id='68ca4aaf', task_id='7cff18da2d979d6370e54a160be121ad', claim='The EU AI Act took effect in March 2024.', language='en', visibility='private', depth='standard', domain='', entities=[], verdict='False', confidence='high', lenz_score=2, key_finding='Finding for The EU AI Act took effect in March 2024.', executive_summary='Summary for The EU AI Act took effect in March 2024.', suggested_rewrite='Fixed: The EU AI Act took effect in March 2024.', warnings=['w1'], created_at='2026-10-01T09:14:00.123456+00:00', modified_at=None, verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', failure=None), suggested_edits=None), ReviewClaim(index=1, claim='Water boils at 100 C at sea level.', positions=None, result=None, assessment=ReviewAssessment(status='failed', verdict=None, confidence=None, rationale=None, dissent=None, verification_url=None, error_code='upstream_unavailable', identified_claims=[], hint=None, suggested_rewrite=None, failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='A model provider was unavailable for this item. Retry it; nothing was charged.', docs_url='https://lenz.io/docs/errors#upstream-unavailable')), escalation=None, verification=None, suggested_edits=None), ReviewClaim(index=2, claim='The Eiffel Tower is in Berlin.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='assessment', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='failed', content_status='available', verification_id=None, task_id='0b92b443eb69b9c71434fdc64f2ca8be', claim=None, language=None, visibility=None, depth=None, domain=None, entities=[], verdict=None, confidence=None, lenz_score=None, key_finding=None, executive_summary=None, suggested_rewrite=None, warnings=[], created_at=None, modified_at=None, verification_url=None, url=None, failure=FailureBlock(failure_reason='research_empty', failure_class='insufficient_evidence', retryable=False, hint=None, docs_url='https://lenz.io/docs/errors#insufficient-evidence')), suggested_edits=None), ReviewClaim(index=3, claim='Paris is the capital of France.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_completed_issues_view_issues.json b/tests/fixtures/parity/expected/review__get_completed_issues_view_issues.json index c0c2c56..ecbfeda 100644 --- a/tests/fixtures/parity/expected/review__get_completed_issues_view_issues.json +++ b/tests/fixtures/parity/expected/review__get_completed_issues_view_issues.json @@ -4,8 +4,8 @@ "citation_issues": [], "citations": [], "claims": [], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 13 }, @@ -41,11 +41,136 @@ "source": "verification", "suggested_edits": null, "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "verified_claim": null + }, + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 2, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "hint": null, + "retryable": false + }, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 3, + "failed": 1 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 4, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 1, + "failed": 1, + "planned": 2 + } + }, + "view": "issues" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 13 + }, + "failure": null, + "failures": [ + { + "claim": "Water boils at 100 C at sea level.", + "claim_index": 1, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + } + ], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The EU AI Act took effect in March 2024.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The EU AI Act took effect in March 2024.", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "verified_claim": null }, { @@ -128,6 +253,7 @@ }, "exit_code": 2, "render": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 failed, 1 not checked · 1 of 2 deep-checked · 13 credits charged\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_issues": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 failed, 1 not checked · 1 of 2 deep-checked · 13 credits charged\n\n[1/4] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\n[3/4] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Deep check failed: research_empty\n\n[2/4] Water boils at 100 C at sea level.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_json": null + "render_issues": "Review d6b2bd72: incomplete — 4 claims: 2 issues, 1 failed, 1 not checked · 1 of 2 deep-checked · 13 credits charged\n\n[1/4] The EU AI Act took effect in March 2024.\n False (high) · deep check\n Finding for The EU AI Act took effect in March 2024.\n Suggested rewrite: Fixed: The EU AI Act took effect in March 2024.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\n[3/4] The Eiffel Tower is in Berlin.\n False (high) · quick check\n Deep check failed: research_empty\n\n[2/4] Water boils at 100 C at sea level.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='issues', status='completed', outcome='incomplete', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=4, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=3, failed=1), verifications=ReviewVerificationCounts(planned=2, completed=1, failed=1), issues=2, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=13), poll_after_seconds=None, issues=[ReviewIssue(claim_index=0, claim='The EU AI Act took effect in March 2024.', verified_claim=None, verdict='False', confidence='high', source='verification', verification_id='68ca4aaf', verification_status='completed', verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding='Finding for The EU AI Act took effect in March 2024.', rationale=None, suggested_rewrite='Fixed: The EU AI Act took effect in March 2024.', failure=None, suggested_edits=None), ReviewIssue(claim_index=2, claim='The Eiffel Tower is in Berlin.', verified_claim=None, verdict='False', confidence='high', source='assessment', verification_id=None, verification_status='failed', verification_url=None, url=None, escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding=None, rationale=None, suggested_rewrite=None, failure=FailureBlock(failure_reason='research_empty', failure_class='insufficient_evidence', retryable=False, hint=None, docs_url='https://lenz.io/docs/errors#insufficient-evidence'), suggested_edits=None)], failures=[ReviewFailure(claim_index=1, claim='Water boils at 100 C at sea level.', stage='assessment', failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='A model provider was unavailable for this item. Retry it; nothing was charged.', docs_url='https://lenz.io/docs/errors#upstream-unavailable'))], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_completed_positions.json b/tests/fixtures/parity/expected/review__get_completed_positions.json index f89f5be..b438323 100644 --- a/tests/fixtures/parity/expected/review__get_completed_positions.json +++ b/tests/fixtures/parity/expected/review__get_completed_positions.json @@ -77,8 +77,142 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [ + { + "end": 40, + "start": 0, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": [ + { + "end": 89, + "start": 55, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -136,5 +270,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024. · at 0-40\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level. · at 55-89\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=[Position(start=0, end=40, text='The EU AI Act took effect in March 2024.')], result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None), ReviewClaim(index=1, claim='Water boils at 100 C at sea level.', positions=[Position(start=55, end=89, text='Water boils at 100 C at sea level.')], result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_completed_recovered_claim.json b/tests/fixtures/parity/expected/review__get_completed_recovered_claim.json index dde95e1..ee17c63 100644 --- a/tests/fixtures/parity/expected/review__get_completed_recovered_claim.json +++ b/tests/fixtures/parity/expected/review__get_completed_recovered_claim.json @@ -35,8 +35,100 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Orbit Motors stock increased by 40% in 2022.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -94,5 +186,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] Orbit Motors stock increased by 40% in 2022.\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=1), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='Orbit Motors stock increased by 40% in 2022.', positions=[], result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_completed_with_untraced_claim.json b/tests/fixtures/parity/expected/review__get_completed_with_untraced_claim.json index cdb1fd6..a57ad21 100644 --- a/tests/fixtures/parity/expected/review__get_completed_with_untraced_claim.json +++ b/tests/fixtures/parity/expected/review__get_completed_with_untraced_claim.json @@ -77,8 +77,142 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [ + { + "end": 40, + "start": 0, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": [ + { + "end": 84, + "start": 50, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -136,5 +270,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024. · at 0-40\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level. · at 50-84\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=[Position(start=0, end=40, text='The EU AI Act took effect in March 2024.')], result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None), ReviewClaim(index=1, claim='Water boils at 100 C at sea level.', positions=[Position(start=50, end=84, text='Water boils at 100 C at sea level.')], result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_during_quick_check.json b/tests/fixtures/parity/expected/review__get_during_quick_check.json index efe850f..f321920 100644 --- a/tests/fixtures/parity/expected/review__get_during_quick_check.json +++ b/tests/fixtures/parity/expected/review__get_during_quick_check.json @@ -28,7 +28,87 @@ } ], "completed_at": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 10, + "review_id": "d6b2bd72", + "status": "assessing", + "summary": { + "assessments": { + "completed": 0, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "running", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": null, + "index": 0, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": null, + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -82,5 +162,6 @@ "exit_code": 2, "render": "Review d6b2bd72: assessing — 1 claim: 1 not checked · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n ? · quick check\n", "render_issues": "Review d6b2bd72: assessing — 1 claim: 1 not checked · 1 credit charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='assessing', outcome=None, created_at='2026-10-01T09:07:00.123456Z', completed_at=None, language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=0, failed=0), verifications=None, issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=1), poll_after_seconds=10, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=None, assessment=ReviewAssessment(status='running', verdict=None, confidence=None, rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=None, verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_failed_every_assessment_failed.json b/tests/fixtures/parity/expected/review__get_failed_every_assessment_failed.json index d9d5414..d7ffa8d 100644 --- a/tests/fixtures/parity/expected/review__get_failed_every_assessment_failed.json +++ b/tests/fixtures/parity/expected/review__get_failed_every_assessment_failed.json @@ -61,8 +61,157 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "assessment_failed", + "hint": "No claim could be assessed. Retry the review with a new Idempotency-Key; nothing was charged.", + "retryable": true + }, + "failures": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + }, + { + "claim": "Water boils at 100 C at sea level.", + "claim_index": 1, + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "stage": "assessment" + } + ], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": { + "completed": 0, + "failed": 2 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": "upstream_unavailable", + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "failed", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": null, + "index": 0, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": null, + "dissent": null, + "error_code": "upstream_unavailable", + "failure": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "failure_class": "upstream_unavailable", + "failure_reason": "upstream_unavailable", + "hint": "A model provider was unavailable for this item. Retry it; nothing was charged.", + "retryable": true + }, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "failed", + "suggested_rewrite": null, + "verdict": null, + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": null, + "index": 1, + "positions": null, + "result": null, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -151,5 +300,6 @@ "exit_code": 2, "render": "Review d6b2bd72: incomplete — 2 claims: 2 failed · 0 credits charged\nFailed: No claim could be assessed. Retry the review with a new Idempotency-Key; nothing was charged.\n\n[1/2] The EU AI Act took effect in March 2024.\n Quick check failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\n[2/2] Water boils at 100 C at sea level.\n Quick check failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n", "render_issues": "Review d6b2bd72: incomplete — 2 claims: 2 failed · 0 credits charged\nFailed: No claim could be assessed. Retry the review with a new Idempotency-Key; nothing was charged.\n\n[1/2] The EU AI Act took effect in March 2024.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n\n[2/2] Water boils at 100 C at sea level.\n Assessment failed: A model provider was unavailable for this item. Retry it; nothing was charged.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='failed', outcome='incomplete', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=0, failed=2), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=0), poll_after_seconds=None, issues=[], failures=[ReviewFailure(claim_index=0, claim='The EU AI Act took effect in March 2024.', stage='assessment', failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='A model provider was unavailable for this item. Retry it; nothing was charged.', docs_url='https://lenz.io/docs/errors#upstream-unavailable')), ReviewFailure(claim_index=1, claim='Water boils at 100 C at sea level.', stage='assessment', failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='A model provider was unavailable for this item. Retry it; nothing was charged.', docs_url='https://lenz.io/docs/errors#upstream-unavailable'))], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=FailureBlock(failure_reason='assessment_failed', failure_class='upstream_unavailable', retryable=True, hint='No claim could be assessed. Retry the review with a new Idempotency-Key; nothing was charged.', docs_url='https://lenz.io/docs/errors#upstream-unavailable'), claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=None, assessment=ReviewAssessment(status='failed', verdict=None, confidence=None, rationale=None, dissent=None, verification_url=None, error_code='upstream_unavailable', identified_claims=[], hint=None, suggested_rewrite=None, failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='A model provider was unavailable for this item. Retry it; nothing was charged.', docs_url='https://lenz.io/docs/errors#upstream-unavailable')), escalation=None, verification=None, suggested_edits=None), ReviewClaim(index=1, claim='Water boils at 100 C at sea level.', positions=None, result=None, assessment=ReviewAssessment(status='failed', verdict=None, confidence=None, rationale=None, dissent=None, verification_url=None, error_code='upstream_unavailable', identified_claims=[], hint=None, suggested_rewrite=None, failure=FailureBlock(failure_reason='upstream_unavailable', failure_class='upstream_unavailable', retryable=True, hint='A model provider was unavailable for this item. Retry it; nothing was charged.', docs_url='https://lenz.io/docs/errors#upstream-unavailable')), escalation=None, verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_failed_no_claim.json b/tests/fixtures/parity/expected/review__get_failed_no_claim.json index 8a2a3a1..831acdf 100644 --- a/tests/fixtures/parity/expected/review__get_failed_no_claim.json +++ b/tests/fixtures/parity/expected/review__get_failed_no_claim.json @@ -4,8 +4,68 @@ "citation_issues": [], "citations": [], "claims": [], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_claim", + "hint": "The text is a greeting, not a statement about the world. Send one factual claim, or run the text through /extract to enumerate its claims.", + "retryable": false + }, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": "unchecked", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -62,5 +122,6 @@ "exit_code": 2, "render": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: The text is a greeting, not a statement about the world. Send one factual claim, or run the text through \n/extract to enumerate its claims.\n", "render_issues": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: The text is a greeting, not a statement about the world. Send one factual claim, or run the text through \n/extract to enumerate its claims.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='failed', outcome='unchecked', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=None, claim_limit=20, claim_limit_reached=None, input_truncated=False, assessments=None, verifications=None, issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=0), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=None, more_claim_locations=None, more_citations=None, failure=FailureBlock(failure_reason='no_claim', failure_class='invalid_input', retryable=False, hint='The text is a greeting, not a statement about the world. Send one factual claim, or run the text through /extract to enumerate its claims.', docs_url='https://lenz.io/docs/errors#invalid-input'), claims=[], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_failed_no_claim_untraced.json b/tests/fixtures/parity/expected/review__get_failed_no_claim_untraced.json index 435f4a6..c371358 100644 --- a/tests/fixtures/parity/expected/review__get_failed_no_claim_untraced.json +++ b/tests/fixtures/parity/expected/review__get_failed_no_claim_untraced.json @@ -4,8 +4,68 @@ "citation_issues": [], "citations": [], "claims": [], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "failure_class": "invalid_input", + "failure_reason": "no_claim", + "hint": "None of the claims found in the text could be traced back to it. Send one factual claim, or run the text through /extract to enumerate its claims.", + "retryable": false + }, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": "unchecked", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -62,5 +122,6 @@ "exit_code": 2, "render": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: None of the claims found in the text could be traced back to it. Send one factual claim, or run the text through\n/extract to enumerate its claims.\n", "render_issues": "Review d6b2bd72: unchecked — no claims checked · 0 credits charged\nFailed: None of the claims found in the text could be traced back to it. Send one factual claim, or run the text through\n/extract to enumerate its claims.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='failed', outcome='unchecked', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=None, claim_limit=20, claim_limit_reached=None, input_truncated=False, assessments=None, verifications=None, issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=0), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=None, more_claim_locations=None, more_citations=None, failure=FailureBlock(failure_reason='no_claim', failure_class='invalid_input', retryable=False, hint='None of the claims found in the text could be traced back to it. Send one factual claim, or run the text through /extract to enumerate its claims.', docs_url='https://lenz.io/docs/errors#invalid-input'), claims=[], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_more_claims.json b/tests/fixtures/parity/expected/review__get_more_claims.json index e22d03e..aa3064b 100644 --- a/tests/fixtures/parity/expected/review__get_more_claims.json +++ b/tests/fixtures/parity/expected/review__get_more_claims.json @@ -77,8 +77,155 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "positions": [ + { + "end": 121, + "start": 91, + "text": "The Eiffel Tower is in Berlin." + } + ] + } + ], + "more_claims": [ + "The Eiffel Tower is in Berlin." + ], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 2, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 2, + "claim_limit_reached": true, + "claims_selected": 2, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [ + { + "end": 40, + "start": 0, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": [ + { + "end": 84, + "start": 50, + "text": "Water boils at 100 C at sea level." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -149,5 +296,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024. · at 0-40\n True (high) · quick check\n\n[2/2] Water boils at 100 C at sea level. · at 50-84\n True (high) · quick check\n\n1 more claim was found but not checked.\n", "render_issues": "Review d6b2bd72: clean — 2 claims: 2 true or mostly true · 2 credits charged\n\nNo issues.\n\n1 more claim was found but not checked.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=2, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=2, claim_limit_reached=True, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=['The Eiffel Tower is in Berlin.'], more_claim_locations=[ClaimLocation(claim='The Eiffel Tower is in Berlin.', positions=[Position(start=91, end=121, text='The Eiffel Tower is in Berlin.')])], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=[Position(start=0, end=40, text='The EU AI Act took effect in March 2024.')], result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None), ReviewClaim(index=1, claim='Water boils at 100 C at sea level.', positions=[Position(start=50, end=84, text='Water boils at 100 C at sea level.')], result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json index 56e09af..71085d6 100644 --- a/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json @@ -50,11 +50,11 @@ "modified_at": "2026-10-02T00:03:00.123456+00:00", "status": "completed", "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "task_id": "7df4009bc27aab64ce5d5a01fbafe599", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "b4e2e99d", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "visibility": "private", "warnings": [ "w1" @@ -62,8 +62,8 @@ } } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 11 }, @@ -86,11 +86,154 @@ "source": "verification", "suggested_edits": null, "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 1, + "failed": 0, + "planned": 1 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "language": "en", + "lenz_score": 2, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "status": "completed", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "visibility": "private", + "warnings": [ + "w1" + ] + } + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 11 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "verified_claim": null } ], @@ -143,7 +286,8 @@ "view": "full" }, "exit_code": 1, - "render": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_json": null + "render": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='issues_found', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=1, completed=1, failed=0), issues=1, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=11), poll_after_seconds=None, issues=[ReviewIssue(claim_index=0, claim='The Eiffel Tower is in Berlin.', verified_claim=None, verdict='False', confidence='high', source='verification', verification_id='68ca4aaf', verification_status='completed', verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding='Finding for The Eiffel Tower is in Berlin.', rationale=None, suggested_rewrite='Fixed: The Eiffel Tower is in Berlin.', failure=None, suggested_edits=None)], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The Eiffel Tower is in Berlin.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='verification', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='completed', content_status='available', verification_id='68ca4aaf', task_id='7cff18da2d979d6370e54a160be121ad', claim='The Eiffel Tower is in Berlin.', language='en', visibility='private', depth='standard', domain='', entities=[], verdict='False', confidence='high', lenz_score=2, key_finding='Finding for The Eiffel Tower is in Berlin.', executive_summary='Summary for The Eiffel Tower is in Berlin.', suggested_rewrite='Fixed: The Eiffel Tower is in Berlin.', warnings=['w1'], created_at='2026-10-01T23:55:00.123456+00:00', modified_at='2026-10-02T00:03:00.123456+00:00', verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', failure=None), suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_same_day_hours_apart.json index ac41e67..554926b 100644 --- a/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/expected/review__get_nested_verification_modified_at_same_day_hours_apart.json @@ -50,11 +50,11 @@ "modified_at": null, "status": "completed", "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "task_id": "7df4009bc27aab64ce5d5a01fbafe599", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "b4e2e99d", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "visibility": "private", "warnings": [ "w1" @@ -62,8 +62,8 @@ } } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 11 }, @@ -86,11 +86,154 @@ "source": "verification", "suggested_edits": null, "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "issues_found", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 1, + "failed": 0, + "planned": 1 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "visibility": "private", + "warnings": [ + "w1" + ] + } + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 11 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": null, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "verified_claim": null } ], @@ -143,7 +286,8 @@ "view": "full" }, "exit_code": 1, - "render": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-b4e2e99d\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_json": null + "render": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: issues found — 1 claim: 1 issue · 1 of 1 deep-checked · 11 credits charged\n\n[1/1] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='issues_found', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=1, completed=1, failed=0), issues=1, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=11), poll_after_seconds=None, issues=[ReviewIssue(claim_index=0, claim='The Eiffel Tower is in Berlin.', verified_claim=None, verdict='False', confidence='high', source='verification', verification_id='68ca4aaf', verification_status='completed', verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding='Finding for The Eiffel Tower is in Berlin.', rationale=None, suggested_rewrite='Fixed: The Eiffel Tower is in Berlin.', failure=None, suggested_edits=None)], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The Eiffel Tower is in Berlin.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='verification', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='completed', content_status='available', verification_id='68ca4aaf', task_id='7cff18da2d979d6370e54a160be121ad', claim='The Eiffel Tower is in Berlin.', language='en', visibility='private', depth='standard', domain='', entities=[], verdict='False', confidence='high', lenz_score=2, key_finding='Finding for The Eiffel Tower is in Berlin.', executive_summary='Summary for The Eiffel Tower is in Berlin.', suggested_rewrite='Fixed: The Eiffel Tower is in Berlin.', warnings=['w1'], created_at='2026-10-01T08:00:00.123456+00:00', modified_at=None, verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', failure=None), suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_policy_resolved.json b/tests/fixtures/parity/expected/review__get_policy_resolved.json index d49a7f7..dc93371 100644 --- a/tests/fixtures/parity/expected/review__get_policy_resolved.json +++ b/tests/fixtures/parity/expected/review__get_policy_resolved.json @@ -35,8 +35,98 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "low", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 2, + "suggest_edits": false, + "verdicts": [ + "False" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -92,5 +182,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False'], confidence=['low'], max_assessments=20, max_verifications=2, depth='low', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=1), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_queued.json b/tests/fixtures/parity/expected/review__get_queued.json index 00606b0..45e6ec0 100644 --- a/tests/fixtures/parity/expected/review__get_queued.json +++ b/tests/fixtures/parity/expected/review__get_queued.json @@ -5,7 +5,61 @@ "citations": [], "claims": [], "completed_at": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": null, + "more_claim_locations": null, + "more_claims": null, + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 10, + "review_id": "d6b2bd72", + "status": "queued", + "summary": { + "assessments": null, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": null, + "claims_selected": null, + "input_truncated": false, + "issues": 0, + "verifications": null + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [], + "completed_at": null, + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -56,5 +110,6 @@ "exit_code": 2, "render": "Review d6b2bd72: queued — no claims checked · 0 credits charged\n", "render_issues": "Review d6b2bd72: queued — no claims checked · 0 credits charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='queued', outcome=None, created_at='2026-10-01T09:07:00.123456Z', completed_at=None, language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=None, claim_limit=20, claim_limit_reached=None, input_truncated=False, assessments=None, verifications=None, issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=0), poll_after_seconds=10, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=None, more_claim_locations=None, more_citations=None, failure=None, claims=[], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_suggested_edits.json b/tests/fixtures/parity/expected/review__get_suggested_edits.json index 9624e1d..5f8332a 100644 --- a/tests/fixtures/parity/expected/review__get_suggested_edits.json +++ b/tests/fixtures/parity/expected/review__get_suggested_edits.json @@ -55,7 +55,7 @@ "claim": "The Eiffel Tower is in Berlin.", "confidence": "high", "content_status": "available", - "created_at": "2026-10-01T09:35:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "depth": "standard", "domain": "", "entities": [], @@ -67,11 +67,11 @@ "modified_at": null, "status": "completed", "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "task_id": "9bca1e0819de60a750186e812dc58e2b", - "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "7c83247c", - "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "visibility": "private", "warnings": [ "w1" @@ -144,7 +144,7 @@ } ], "completed_at": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 12 }, @@ -178,11 +178,272 @@ "status": "completed" }, "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "7c83247c", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "verified_claim": null + }, + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 1, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": { + "edits": null, + "status": "pending" + }, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": true, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 15, + "review_id": "d6b2bd72", + "status": "verifying", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 1, + "failed": 0, + "planned": 2 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": [ + { + "end": 30, + "start": 0, + "text": "The Eiffel Tower is in Berlin." + } + ], + "result": { + "confidence": "high", + "is_issue": true, + "source": "verification", + "verdict": "False" + }, + "suggested_edits": { + "edits": [ + { + "end": 29, + "position": 0, + "replacement": "Paris", + "start": 23, + "text": "Berlin" + } + ], + "status": "completed" + }, + "verification": { + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "high", + "content_status": "available", + "created_at": "2026-10-01T09:14:00.123456+00:00", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "Summary for The Eiffel Tower is in Berlin.", + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "language": "en", + "lenz_score": 2, + "modified_at": null, + "status": "completed", + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "visibility": "private", + "warnings": [ + "w1" + ] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 1, + "positions": [ + { + "end": 85, + "start": 45, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": { + "edits": null, + "status": "pending" + }, + "verification": { + "claim": null, + "confidence": null, + "content_status": "available", + "created_at": null, + "depth": null, + "domain": null, + "entities": [], + "executive_summary": null, + "failure": null, + "key_finding": null, + "language": null, + "lenz_score": null, + "modified_at": null, + "status": "processing", + "suggested_rewrite": null, + "task_id": null, + "url": null, + "verdict": null, + "verification_id": null, + "verification_url": null, + "visibility": null, + "warnings": [] + } + } + ], + "completed_at": null, + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 12 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": { + "edits": [ + { + "end": 29, + "position": 0, + "replacement": "Paris", + "start": 23, + "text": "Berlin" + } + ], + "status": "completed" + }, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "verified_claim": null }, { @@ -261,7 +522,8 @@ "view": "full" }, "exit_code": 2, - "render": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\n[1/2] The Eiffel Tower is in Berlin. · at 0-30\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-7c83247c\n\n[2/2] The EU AI Act took effect in March 2024. · at 45-85\n False (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_issues": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\n[1/2] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-7c83247c\n\n[2/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_json": null + "render": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\n[1/2] The Eiffel Tower is in Berlin. · at 0-30\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\n[2/2] The EU AI Act took effect in March 2024. · at 45-85\n False (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_issues": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\n[1/2] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\n[2/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='verifying', outcome=None, created_at='2026-10-01T09:07:00.123456Z', completed_at=None, language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=True), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=2, completed=1, failed=0), issues=2, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=12), poll_after_seconds=15, issues=[ReviewIssue(claim_index=0, claim='The Eiffel Tower is in Berlin.', verified_claim=None, verdict='False', confidence='high', source='verification', verification_id='68ca4aaf', verification_status='completed', verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding='Finding for The Eiffel Tower is in Berlin.', rationale=None, suggested_rewrite='Fixed: The Eiffel Tower is in Berlin.', failure=None, suggested_edits=SuggestedEdits(status='completed', edits=[SuggestedEdit(position=0, start=23, end=29, text='Berlin', replacement='Paris')])), ReviewIssue(claim_index=1, claim='The EU AI Act took effect in March 2024.', verified_claim=None, verdict='False', confidence='high', source='assessment', verification_id=None, verification_status='processing', verification_url=None, url=None, escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding=None, rationale=None, suggested_rewrite=None, failure=None, suggested_edits=SuggestedEdits(status='pending', edits=None))], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The Eiffel Tower is in Berlin.', positions=[Position(start=0, end=30, text='The Eiffel Tower is in Berlin.')], result=ReviewResult(verdict='False', confidence='high', source='verification', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='completed', content_status='available', verification_id='68ca4aaf', task_id='7cff18da2d979d6370e54a160be121ad', claim='The Eiffel Tower is in Berlin.', language='en', visibility='private', depth='standard', domain='', entities=[], verdict='False', confidence='high', lenz_score=2, key_finding='Finding for The Eiffel Tower is in Berlin.', executive_summary='Summary for The Eiffel Tower is in Berlin.', suggested_rewrite='Fixed: The Eiffel Tower is in Berlin.', warnings=['w1'], created_at='2026-10-01T09:14:00.123456+00:00', modified_at=None, verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', failure=None), suggested_edits=SuggestedEdits(status='completed', edits=[SuggestedEdit(position=0, start=23, end=29, text='Berlin', replacement='Paris')])), ReviewClaim(index=1, claim='The EU AI Act took effect in March 2024.', positions=[Position(start=45, end=85, text='The EU AI Act took effect in March 2024.')], result=ReviewResult(verdict='False', confidence='high', source='assessment', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='processing', content_status='available', verification_id=None, task_id=None, claim=None, language=None, visibility=None, depth=None, domain=None, entities=[], verdict=None, confidence=None, lenz_score=None, key_finding=None, executive_summary=None, suggested_rewrite=None, warnings=[], created_at=None, modified_at=None, verification_url=None, url=None, failure=None), suggested_edits=SuggestedEdits(status='pending', edits=None))], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_suggested_edits_issues_view.json b/tests/fixtures/parity/expected/review__get_suggested_edits_issues_view.json index 2f8589c..ec30607 100644 --- a/tests/fixtures/parity/expected/review__get_suggested_edits_issues_view.json +++ b/tests/fixtures/parity/expected/review__get_suggested_edits_issues_view.json @@ -5,7 +5,7 @@ "citations": [], "claims": [], "completed_at": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 12 }, @@ -39,11 +39,131 @@ "status": "completed" }, "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "7c83247c", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "verified_claim": null + }, + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 1, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": { + "edits": null, + "status": "pending" + }, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": true, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 15, + "review_id": "d6b2bd72", + "status": "verifying", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 2, + "verifications": { + "completed": 1, + "failed": 0, + "planned": 2 + } + }, + "view": "issues" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "completed_at": null, + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 12 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": "Finding for The Eiffel Tower is in Berlin.", + "rationale": null, + "source": "verification", + "suggested_edits": { + "edits": [ + { + "end": 29, + "position": 0, + "replacement": "Paris", + "start": 23, + "text": "Berlin" + } + ], + "status": "completed" + }, + "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", + "verdict": "False", + "verification_id": "68ca4aaf", + "verification_status": "completed", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "verified_claim": null }, { @@ -123,6 +243,7 @@ }, "exit_code": 2, "render": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_issues": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\n[1/2] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-7c83247c\n\n[2/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", - "render_json": null + "render_issues": "Review d6b2bd72: verifying — 2 claims: 2 issues · 1 of 2 deep-checked · 12 credits charged\n\n[1/2] The Eiffel Tower is in Berlin.\n False (high) · deep check\n Finding for The Eiffel Tower is in Berlin.\n Suggested rewrite: Fixed: The Eiffel Tower is in Berlin.\n https://lenz.io/c/the-earth-is-round-68ca4aaf\n\n[2/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n\nA suggested rewrite has not been verified itself: review it, or run it through `lenz verify`, before you use it.\n", + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='issues', status='verifying', outcome=None, created_at='2026-10-01T09:07:00.123456Z', completed_at=None, language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=True), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=2, completed=1, failed=0), issues=2, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=12), poll_after_seconds=15, issues=[ReviewIssue(claim_index=0, claim='The Eiffel Tower is in Berlin.', verified_claim=None, verdict='False', confidence='high', source='verification', verification_id='68ca4aaf', verification_status='completed', verification_url='https://lenz.io/api/v1/verifications/68ca4aaf', url='https://lenz.io/c/the-earth-is-round-68ca4aaf', escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding='Finding for The Eiffel Tower is in Berlin.', rationale=None, suggested_rewrite='Fixed: The Eiffel Tower is in Berlin.', failure=None, suggested_edits=SuggestedEdits(status='completed', edits=[SuggestedEdit(position=0, start=23, end=29, text='Berlin', replacement='Paris')])), ReviewIssue(claim_index=1, claim='The EU AI Act took effect in March 2024.', verified_claim=None, verdict='False', confidence='high', source='assessment', verification_id=None, verification_status='processing', verification_url=None, url=None, escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding=None, rationale=None, suggested_rewrite=None, failure=None, suggested_edits=SuggestedEdits(status='pending', edits=None))], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_url_review.json b/tests/fixtures/parity/expected/review__get_url_review.json index ea99aa8..8153a7d 100644 --- a/tests/fixtures/parity/expected/review__get_url_review.json +++ b/tests/fixtures/parity/expected/review__get_url_review.json @@ -41,8 +41,106 @@ "verification": null } ], - "completed_at": "2026-10-01T09:21:00.123456Z", - "created_at": "2026-10-01T09:14:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": null, + "failures": [], + "issues": [], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "clean", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "completed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 0, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 0 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 0, + "positions": [ + { + "end": null, + "start": null, + "text": "The EU AI Act took effect in March 2024." + } + ], + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -100,5 +198,6 @@ "exit_code": 0, "render": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\n[1/1] The EU AI Act took effect in March 2024.\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: clean — 1 claim: 1 true or mostly true · 1 credit charged\n\nNo issues.\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='completed', outcome='clean', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=1, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=1, failed=0), verifications=ReviewVerificationCounts(planned=0, completed=0, failed=0), issues=0, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=1), poll_after_seconds=None, issues=[], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=[Position(start=None, end=None, text='The EU AI Act took effect in March 2024.')], result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__get_verifying.json b/tests/fixtures/parity/expected/review__get_verifying.json index 0b901c0..63b031e 100644 --- a/tests/fixtures/parity/expected/review__get_verifying.json +++ b/tests/fixtures/parity/expected/review__get_verifying.json @@ -91,7 +91,178 @@ } ], "completed_at": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": null, + "failures": [], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": null, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "processing", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": null, + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": 15, + "review_id": "d6b2bd72", + "status": "verifying", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 0, + "failed": 0, + "planned": 1 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": null, + "confidence": null, + "content_status": "available", + "created_at": null, + "depth": null, + "domain": null, + "entities": [], + "executive_summary": null, + "failure": null, + "key_finding": null, + "language": null, + "lenz_score": null, + "modified_at": null, + "status": "processing", + "suggested_rewrite": null, + "task_id": null, + "url": null, + "verdict": null, + "verification_id": null, + "verification_url": null, + "visibility": null, + "warnings": [] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Water boils at 100 C at sea level.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": null, + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -173,5 +344,6 @@ "exit_code": 2, "render": "Review d6b2bd72: verifying — 2 claims: 1 issue, 1 true or mostly true · 0 of 1 deep-checked · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n\n[2/2] Water boils at 100 C at sea level.\n True (high) · quick check\n", "render_issues": "Review d6b2bd72: verifying — 2 claims: 1 issue, 1 true or mostly true · 0 of 1 deep-checked · 2 credits charged\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n", - "render_json": null + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='verifying', outcome=None, created_at='2026-10-01T09:07:00.123456Z', completed_at=None, language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=1, completed=0, failed=0), issues=1, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=15, issues=[ReviewIssue(claim_index=0, claim='The EU AI Act took effect in March 2024.', verified_claim=None, verdict='False', confidence='high', source='assessment', verification_id=None, verification_status='processing', verification_url=None, url=None, escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding=None, rationale=None, suggested_rewrite=None, failure=None, suggested_edits=None)], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=None, claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='assessment', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='processing', content_status='available', verification_id=None, task_id=None, claim=None, language=None, visibility=None, depth=None, domain=None, entities=[], verdict=None, confidence=None, lenz_score=None, key_finding=None, executive_summary=None, suggested_rewrite=None, warnings=[], created_at=None, modified_at=None, verification_url=None, url=None, failure=None), suggested_edits=None), ReviewClaim(index=1, claim='Water boils at 100 C at sea level.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" } diff --git a/tests/fixtures/parity/expected/review__idempotent_replay_202.json b/tests/fixtures/parity/expected/review__idempotent_replay_202.json index 17e07a5..615c995 100644 --- a/tests/fixtures/parity/expected/review__idempotent_replay_202.json +++ b/tests/fixtures/parity/expected/review__idempotent_replay_202.json @@ -3,5 +3,10 @@ "review_id": "d6b2bd72", "status": "queued" }, - "render_json": null + "dump_unset": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null, + "repr": "ReviewStarted(review_id='d6b2bd72', status='queued')" } diff --git a/tests/fixtures/parity/expected/review__receipt_202.json b/tests/fixtures/parity/expected/review__receipt_202.json index 17e07a5..615c995 100644 --- a/tests/fixtures/parity/expected/review__receipt_202.json +++ b/tests/fixtures/parity/expected/review__receipt_202.json @@ -3,5 +3,10 @@ "review_id": "d6b2bd72", "status": "queued" }, - "render_json": null + "dump_unset": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null, + "repr": "ReviewStarted(review_id='d6b2bd72', status='queued')" } diff --git a/tests/fixtures/parity/expected/review__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/expected/review__receipt_202_empty_webhook_url.json index 17e07a5..615c995 100644 --- a/tests/fixtures/parity/expected/review__receipt_202_empty_webhook_url.json +++ b/tests/fixtures/parity/expected/review__receipt_202_empty_webhook_url.json @@ -3,5 +3,10 @@ "review_id": "d6b2bd72", "status": "queued" }, - "render_json": null + "dump_unset": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null, + "repr": "ReviewStarted(review_id='d6b2bd72', status='queued')" } diff --git a/tests/fixtures/parity/expected/review__receipt_202_webhook_url.json b/tests/fixtures/parity/expected/review__receipt_202_webhook_url.json index 17e07a5..615c995 100644 --- a/tests/fixtures/parity/expected/review__receipt_202_webhook_url.json +++ b/tests/fixtures/parity/expected/review__receipt_202_webhook_url.json @@ -3,5 +3,10 @@ "review_id": "d6b2bd72", "status": "queued" }, - "render_json": null + "dump_unset": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null, + "repr": "ReviewStarted(review_id='d6b2bd72', status='queued')" } diff --git a/tests/fixtures/parity/expected/review__replay_later_202.json b/tests/fixtures/parity/expected/review__replay_later_202.json index 17e07a5..615c995 100644 --- a/tests/fixtures/parity/expected/review__replay_later_202.json +++ b/tests/fixtures/parity/expected/review__replay_later_202.json @@ -3,5 +3,10 @@ "review_id": "d6b2bd72", "status": "queued" }, - "render_json": null + "dump_unset": { + "review_id": "d6b2bd72", + "status": "queued" + }, + "render_json": null, + "repr": "ReviewStarted(review_id='d6b2bd72', status='queued')" } diff --git a/tests/fixtures/parity/expected/verify__batch_202.json b/tests/fixtures/parity/expected/verify__batch_202.json index d044b7c..1f9894c 100644 --- a/tests/fixtures/parity/expected/verify__batch_202.json +++ b/tests/fixtures/parity/expected/verify__batch_202.json @@ -4,13 +4,27 @@ "items": [ { "claim_text": "The Earth is round.", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + { + "claim_text": "Water boils at 100C at sea level.", "task_id": "0b92b443eb69b9c71434fdc64f2ca8be" + } + ] + }, + "dump_unset": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "claim_text": "The Earth is round.", + "task_id": "7cff18da2d979d6370e54a160be121ad" }, { "claim_text": "Water boils at 100C at sea level.", - "task_id": "13610f0b64b63e006dc66364e0e82c85" + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be" } ] }, - "render_json": null + "render_json": null, + "repr": "BatchAccepted(batch_id='7ea9cdbd4064e3cf', items=[TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='The Earth is round.'), TaskAccepted(task_id='0b92b443eb69b9c71434fdc64f2ca8be', claim_text='Water boils at 100C at sea level.')])" } diff --git a/tests/fixtures/parity/expected/verify__batch_no_credits_402.json b/tests/fixtures/parity/expected/verify__batch_no_credits_402.json index 806620a..52d04da 100644 --- a/tests/fixtures/parity/expected/verify__batch_no_credits_402.json +++ b/tests/fixtures/parity/expected/verify__batch_no_credits_402.json @@ -20,7 +20,7 @@ "remaining": 0, "request_id": "", "requested": 1, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/verify__batch_partial_202.json b/tests/fixtures/parity/expected/verify__batch_partial_202.json index 96fb74b..8a4a0ba 100644 --- a/tests/fixtures/parity/expected/verify__batch_partial_202.json +++ b/tests/fixtures/parity/expected/verify__batch_partial_202.json @@ -4,10 +4,21 @@ "items": [ { "claim_text": "First claim.", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be" + "task_id": "7cff18da2d979d6370e54a160be121ad" } ], "partial": true }, - "render_json": null + "dump_unset": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "claim_text": "First claim.", + "task_id": "7cff18da2d979d6370e54a160be121ad" + } + ], + "partial": true + }, + "render_json": null, + "repr": "BatchAccepted(batch_id='7ea9cdbd4064e3cf', items=[TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='First claim.')], partial=True)" } diff --git a/tests/fixtures/parity/expected/verify__delete_200.json b/tests/fixtures/parity/expected/verify__delete_200.json new file mode 100644 index 0000000..a75769b --- /dev/null +++ b/tests/fixtures/parity/expected/verify__delete_200.json @@ -0,0 +1,12 @@ +{ + "dump": { + "claim_text": "", + "ok": true, + "task_id": "" + }, + "dump_unset": { + "ok": true + }, + "render_json": null, + "repr": "TaskAccepted(task_id='', claim_text='', ok=True)" +} diff --git a/tests/fixtures/parity/expected/verify__delete_404.json b/tests/fixtures/parity/expected/verify__delete_404.json new file mode 100644 index 0000000..41573fe --- /dev/null +++ b/tests/fixtures/parity/expected/verify__delete_404.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__delete_not_yours_404.json b/tests/fixtures/parity/expected/verify__delete_not_yours_404.json new file mode 100644 index 0000000..41573fe --- /dev/null +++ b/tests/fixtures/parity/expected/verify__delete_not_yours_404.json @@ -0,0 +1,21 @@ +{ + "error": { + "cause": "Not found.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "friendly_text": "Not found.\n Fix: Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "payload_json": { + "error": { + "code": "api_error", + "fix": "Retry; if the error persists, file an issue with the Request ID.", + "message": "Not found.", + "status": 404 + } + }, + "request_id": "", + "status_code": 404, + "type": "LenzError" + } +} diff --git a/tests/fixtures/parity/expected/verify__idempotency_key_replay.json b/tests/fixtures/parity/expected/verify__idempotency_key_replay.json index 4aa7f3a..74a0269 100644 --- a/tests/fixtures/parity/expected/verify__idempotency_key_replay.json +++ b/tests/fixtures/parity/expected/verify__idempotency_key_replay.json @@ -1,9 +1,15 @@ { "dump": { - "chain_id": "78ac4e9f234ea3bd", + "chain_id": "99a0a9ca89e1e72c", "claim_text": "", "status": "queued", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render_json": null + "dump_unset": { + "chain_id": "99a0a9ca89e1e72c", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null, + "repr": "TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='', status='queued', chain_id='99a0a9ca89e1e72c')" } diff --git a/tests/fixtures/parity/expected/verify__implicit_dedup_200.json b/tests/fixtures/parity/expected/verify__implicit_dedup_200.json index 77b6440..a7ae9ac 100644 --- a/tests/fixtures/parity/expected/verify__implicit_dedup_200.json +++ b/tests/fixtures/parity/expected/verify__implicit_dedup_200.json @@ -4,5 +4,10 @@ "status": "queued", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render_json": null + "dump_unset": { + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null, + "repr": "TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='', status='queued')" } diff --git a/tests/fixtures/parity/expected/verify__implicit_repeat_replay.json b/tests/fixtures/parity/expected/verify__implicit_repeat_replay.json index 4aa7f3a..74a0269 100644 --- a/tests/fixtures/parity/expected/verify__implicit_repeat_replay.json +++ b/tests/fixtures/parity/expected/verify__implicit_repeat_replay.json @@ -1,9 +1,15 @@ { "dump": { - "chain_id": "78ac4e9f234ea3bd", + "chain_id": "99a0a9ca89e1e72c", "claim_text": "", "status": "queued", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render_json": null + "dump_unset": { + "chain_id": "99a0a9ca89e1e72c", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null, + "repr": "TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='', status='queued', chain_id='99a0a9ca89e1e72c')" } diff --git a/tests/fixtures/parity/expected/verify__list_200.json b/tests/fixtures/parity/expected/verify__list_200.json index 9b7f3a5..ce1a3f5 100644 --- a/tests/fixtures/parity/expected/verify__list_200.json +++ b/tests/fixtures/parity/expected/verify__list_200.json @@ -1,6 +1,21 @@ { "dump": { "items": [ + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + }, { "claim": "The Earth is round.", "confidence": "high", @@ -14,12 +29,34 @@ "modified_at": null, "suggested_rewrite": null, "verdict": "True", + "verification_id": "4d44dfaf" + } + ], + "page": 1, + "page_size": 20, + "total": 2 + }, + "dump_unset": { + "items": [ + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", "verification_id": "68ca4aaf" }, { "claim": "The Earth is round.", "confidence": "high", - "created_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "domain": "Science", "entities": [], "executive_summary": "The claim is verified.", @@ -29,12 +66,13 @@ "modified_at": null, "suggested_rewrite": null, "verdict": "True", - "verification_id": "7c83247c" + "verification_id": "4d44dfaf" } ], "page": 1, "page_size": 20, "total": 2 }, - "render_json": null + "render_json": null, + "repr": "VerificationList(items=[VerificationListItem(verification_id='68ca4aaf', claim='The Earth is round.', domain='Science', entities=[], verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en'), VerificationListItem(verification_id='4d44dfaf', claim='The Earth is round.', domain='Science', entities=[], verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T09:14:00.123456+00:00', modified_at=None, language='en')], total=2, page=1, page_size=20)" } diff --git a/tests/fixtures/parity/expected/verify__list_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/verify__list_200_modified_at_crosses_midnight_by_minutes.json index 8550130..0483d52 100644 --- a/tests/fixtures/parity/expected/verify__list_200_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/expected/verify__list_200_modified_at_crosses_midnight_by_minutes.json @@ -21,5 +21,28 @@ "page_size": 20, "total": 1 }, - "render_json": null + "dump_unset": { + "items": [ + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T23:55:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 1, + "page_size": 20, + "total": 1 + }, + "render_json": null, + "repr": "VerificationList(items=[VerificationListItem(verification_id='68ca4aaf', claim='The Earth is round.', domain='Science', entities=[], verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T23:55:00.123456+00:00', modified_at='2026-10-02T00:03:00.123456+00:00', language='en')], total=1, page=1, page_size=20)" } diff --git a/tests/fixtures/parity/expected/verify__list_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/verify__list_200_modified_at_same_day_hours_apart.json index b333318..43c5712 100644 --- a/tests/fixtures/parity/expected/verify__list_200_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/expected/verify__list_200_modified_at_same_day_hours_apart.json @@ -21,5 +21,28 @@ "page_size": 20, "total": 1 }, - "render_json": null + "dump_unset": { + "items": [ + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T08:00:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 1, + "page_size": 20, + "total": 1 + }, + "render_json": null, + "repr": "VerificationList(items=[VerificationListItem(verification_id='68ca4aaf', claim='The Earth is round.', domain='Science', entities=[], verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T08:00:00.123456+00:00', modified_at=None, language='en')], total=1, page=1, page_size=20)" } diff --git a/tests/fixtures/parity/expected/verify__list_clamped_200.json b/tests/fixtures/parity/expected/verify__list_clamped_200.json index b48e6be..5f47a1d 100644 --- a/tests/fixtures/parity/expected/verify__list_clamped_200.json +++ b/tests/fixtures/parity/expected/verify__list_clamped_200.json @@ -5,5 +5,12 @@ "page_size": 100, "total": 0 }, - "render_json": null + "dump_unset": { + "items": [], + "page": 1, + "page_size": 100, + "total": 0 + }, + "render_json": null, + "repr": "VerificationList(items=[], total=0, page=1, page_size=100)" } diff --git a/tests/fixtures/parity/expected/verify__list_empty_200.json b/tests/fixtures/parity/expected/verify__list_empty_200.json index d628806..a0747d2 100644 --- a/tests/fixtures/parity/expected/verify__list_empty_200.json +++ b/tests/fixtures/parity/expected/verify__list_empty_200.json @@ -5,5 +5,12 @@ "page_size": 20, "total": 0 }, - "render_json": null + "dump_unset": { + "items": [], + "page": 1, + "page_size": 20, + "total": 0 + }, + "render_json": null, + "repr": "VerificationList(items=[], total=0, page=1, page_size=20)" } diff --git a/tests/fixtures/parity/expected/verify__list_page2_200.json b/tests/fixtures/parity/expected/verify__list_page2_200.json index c5aab9a..e2682ef 100644 --- a/tests/fixtures/parity/expected/verify__list_page2_200.json +++ b/tests/fixtures/parity/expected/verify__list_page2_200.json @@ -4,7 +4,7 @@ { "claim": "The Earth is round.", "confidence": "high", - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "domain": "Science", "entities": [], "executive_summary": "The claim is verified.", @@ -21,5 +21,28 @@ "page_size": 2, "total": 3 }, - "render_json": null + "dump_unset": { + "items": [ + { + "claim": "The Earth is round.", + "confidence": "high", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf" + } + ], + "page": 2, + "page_size": 2, + "total": 3 + }, + "render_json": null, + "repr": "VerificationList(items=[VerificationListItem(verification_id='68ca4aaf', claim='The Earth is round.', domain='Science', entities=[], verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', suggested_rewrite=None, created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en')], total=3, page=2, page_size=2)" } diff --git a/tests/fixtures/parity/expected/verify__no_credits_402.json b/tests/fixtures/parity/expected/verify__no_credits_402.json index f3a12a3..f16364a 100644 --- a/tests/fixtures/parity/expected/verify__no_credits_402.json +++ b/tests/fixtures/parity/expected/verify__no_credits_402.json @@ -20,7 +20,7 @@ "remaining": 0, "request_id": "", "requested": null, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "status_code": 402, "type": "LenzQuotaExceededError", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57" diff --git a/tests/fixtures/parity/expected/verify__replay_later_202.json b/tests/fixtures/parity/expected/verify__replay_later_202.json index 4aa7f3a..74a0269 100644 --- a/tests/fixtures/parity/expected/verify__replay_later_202.json +++ b/tests/fixtures/parity/expected/verify__replay_later_202.json @@ -1,9 +1,15 @@ { "dump": { - "chain_id": "78ac4e9f234ea3bd", + "chain_id": "99a0a9ca89e1e72c", "claim_text": "", "status": "queued", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render_json": null + "dump_unset": { + "chain_id": "99a0a9ca89e1e72c", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null, + "repr": "TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='', status='queued', chain_id='99a0a9ca89e1e72c')" } diff --git a/tests/fixtures/parity/expected/verify__select_202.json b/tests/fixtures/parity/expected/verify__select_202.json index d044b7c..1f9894c 100644 --- a/tests/fixtures/parity/expected/verify__select_202.json +++ b/tests/fixtures/parity/expected/verify__select_202.json @@ -4,13 +4,27 @@ "items": [ { "claim_text": "The Earth is round.", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + { + "claim_text": "Water boils at 100C at sea level.", "task_id": "0b92b443eb69b9c71434fdc64f2ca8be" + } + ] + }, + "dump_unset": { + "batch_id": "7ea9cdbd4064e3cf", + "items": [ + { + "claim_text": "The Earth is round.", + "task_id": "7cff18da2d979d6370e54a160be121ad" }, { "claim_text": "Water boils at 100C at sea level.", - "task_id": "13610f0b64b63e006dc66364e0e82c85" + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be" } ] }, - "render_json": null + "render_json": null, + "repr": "BatchAccepted(batch_id='7ea9cdbd4064e3cf', items=[TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='The Earth is round.'), TaskAccepted(task_id='0b92b443eb69b9c71434fdc64f2ca8be', claim_text='Water boils at 100C at sea level.')])" } diff --git a/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json b/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json index 0c51d3c..820dfc6 100644 --- a/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json +++ b/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json @@ -22,8 +22,18 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "error": "Cancelled.", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — Cancelled.\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Cancelled.\",\n \"failure_reason\": \"cancelled\",\n \"failure_detail\": \"\",\n \"failure_class\": \"cancelled\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#cancelled\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Cancelled.', failure_reason='cancelled', failure_detail='', failure_class='cancelled', retryable=False, docs_url='https://lenz.io/docs/errors#cancelled')", "wait": { "cause": "Cancelled.", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_completed.json b/tests/fixtures/parity/expected/verify__status_completed.json index 4455d97..9f88760 100644 --- a/tests/fixtures/parity/expected/verify__status_completed.json +++ b/tests/fixtures/parity/expected/verify__status_completed.json @@ -64,7 +64,7 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -97,7 +97,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" @@ -108,9 +108,101 @@ "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", - "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:21:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", - "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "dump_unset": { + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 68ca4aaf\nfull report: lenz show 68ca4aaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "repr": "TaskStatus(status='completed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1')), claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "returned": { "audit": { @@ -160,7 +252,7 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -193,7 +285,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" diff --git a/tests/fixtures/parity/expected/verify__status_completed_after_a_while.json b/tests/fixtures/parity/expected/verify__status_completed_after_a_while.json index 4455d97..9f88760 100644 --- a/tests/fixtures/parity/expected/verify__status_completed_after_a_while.json +++ b/tests/fixtures/parity/expected/verify__status_completed_after_a_while.json @@ -64,7 +64,7 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -97,7 +97,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" @@ -108,9 +108,101 @@ "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", - "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:21:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", - "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "dump_unset": { + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 68ca4aaf\nfull report: lenz show 68ca4aaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "repr": "TaskStatus(status='completed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1')), claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "returned": { "audit": { @@ -160,7 +252,7 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -193,7 +285,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" diff --git a/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json index 3b77489..fd6df30 100644 --- a/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json @@ -97,7 +97,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" @@ -108,9 +108,101 @@ "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", - "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", - "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "dump_unset": { + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 68ca4aaf\nfull report: lenz show 68ca4aaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "repr": "TaskStatus(status='completed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T23:55:00.123456+00:00', modified_at='2026-10-02T00:03:00.123456+00:00', language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1')), claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "returned": { "audit": { @@ -193,7 +285,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" diff --git a/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json index 29f1593..b935bd9 100644 --- a/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/expected/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json @@ -97,7 +97,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" @@ -108,9 +108,101 @@ "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", - "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T08:00:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", - "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "dump_unset": { + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 68ca4aaf\nfull report: lenz show 68ca4aaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T08:00:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "repr": "TaskStatus(status='completed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T08:00:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1')), claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "returned": { "audit": { @@ -193,7 +285,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" diff --git a/tests/fixtures/parity/expected/verify__status_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/verify__status_completed_modified_at_crosses_midnight_by_minutes.json index 3b77489..fd6df30 100644 --- a/tests/fixtures/parity/expected/verify__status_completed_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/expected/verify__status_completed_modified_at_crosses_midnight_by_minutes.json @@ -97,7 +97,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" @@ -108,9 +108,101 @@ "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", - "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", - "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "dump_unset": { + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 68ca4aaf\nfull report: lenz show 68ca4aaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "repr": "TaskStatus(status='completed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T23:55:00.123456+00:00', modified_at='2026-10-02T00:03:00.123456+00:00', language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1')), claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "returned": { "audit": { @@ -193,7 +285,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" diff --git a/tests/fixtures/parity/expected/verify__status_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/verify__status_completed_modified_at_same_day_hours_apart.json index 29f1593..b935bd9 100644 --- a/tests/fixtures/parity/expected/verify__status_completed_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/expected/verify__status_completed_modified_at_same_day_hours_apart.json @@ -97,7 +97,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" @@ -108,9 +108,101 @@ "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", - "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T08:00:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", - "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "dump_unset": { + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 68ca4aaf\nfull report: lenz show 68ca4aaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T08:00:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "repr": "TaskStatus(status='completed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T08:00:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1')), claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "returned": { "audit": { @@ -193,7 +285,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" diff --git a/tests/fixtures/parity/expected/verify__status_failed.json b/tests/fixtures/parity/expected/verify__status_failed.json index c379a8d..c550bd3 100644 --- a/tests/fixtures/parity/expected/verify__status_failed.json +++ b/tests/fixtures/parity/expected/verify__status_failed.json @@ -22,8 +22,18 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "error": "Pipeline stopped at: research_empty", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — Pipeline stopped at: research_empty\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped at: research_empty\",\n \"failure_reason\": \"research_empty\",\n \"failure_detail\": \"\",\n \"failure_class\": \"insufficient_evidence\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#insufficient-evidence\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Pipeline stopped at: research_empty', failure_reason='research_empty', failure_detail='', failure_class='insufficient_evidence', retryable=False, docs_url='https://lenz.io/docs/errors#insufficient-evidence')", "wait": { "cause": "Pipeline stopped at: research_empty", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_failed_after_a_while.json b/tests/fixtures/parity/expected/verify__status_failed_after_a_while.json index 1de2af6..4714bed 100644 --- a/tests/fixtures/parity/expected/verify__status_failed_after_a_while.json +++ b/tests/fixtures/parity/expected/verify__status_failed_after_a_while.json @@ -22,8 +22,18 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#internal", + "error": "Pipeline stopped: conclusion_failed.", + "failure_class": "internal", + "failure_reason": "conclusion_failed", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — Pipeline stopped: conclusion_failed.\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped: conclusion_failed.\",\n \"failure_reason\": \"conclusion_failed\",\n \"failure_detail\": \"\",\n \"failure_class\": \"internal\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#internal\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Pipeline stopped: conclusion_failed.', failure_reason='conclusion_failed', failure_detail='', failure_class='internal', retryable=False, docs_url='https://lenz.io/docs/errors#internal')", "wait": { "cause": "Pipeline stopped: conclusion_failed.", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_failed_after_a_while_framing.json b/tests/fixtures/parity/expected/verify__status_failed_after_a_while_framing.json index 52d9c84..635cdf9 100644 --- a/tests/fixtures/parity/expected/verify__status_failed_after_a_while_framing.json +++ b/tests/fixtures/parity/expected/verify__status_failed_after_a_while_framing.json @@ -22,8 +22,18 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "error": "Pipeline stopped: framing_failed.", + "failure_class": "upstream_unavailable", + "failure_reason": "framing_failed", + "retryable": true, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — Pipeline stopped: framing_failed.\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped: framing_failed.\",\n \"failure_reason\": \"framing_failed\",\n \"failure_detail\": \"\",\n \"failure_class\": \"upstream_unavailable\",\n \"retryable\": true,\n \"docs_url\": \"https://lenz.io/docs/errors#upstream-unavailable\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Pipeline stopped: framing_failed.', failure_reason='framing_failed', failure_detail='', failure_class='upstream_unavailable', retryable=True, docs_url='https://lenz.io/docs/errors#upstream-unavailable')", "wait": { "cause": "Pipeline stopped: framing_failed.", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_failed_retryable.json b/tests/fixtures/parity/expected/verify__status_failed_retryable.json index fbe1bf9..f49b1cc 100644 --- a/tests/fixtures/parity/expected/verify__status_failed_retryable.json +++ b/tests/fixtures/parity/expected/verify__status_failed_retryable.json @@ -22,8 +22,18 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "error": "Pipeline stopped at: adjudication_failed", + "failure_class": "upstream_unavailable", + "failure_reason": "adjudication_failed", + "retryable": true, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — Pipeline stopped at: adjudication_failed\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped at: adjudication_failed\",\n \"failure_reason\": \"adjudication_failed\",\n \"failure_detail\": \"\",\n \"failure_class\": \"upstream_unavailable\",\n \"retryable\": true,\n \"docs_url\": \"https://lenz.io/docs/errors#upstream-unavailable\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Pipeline stopped at: adjudication_failed', failure_reason='adjudication_failed', failure_detail='', failure_class='upstream_unavailable', retryable=True, docs_url='https://lenz.io/docs/errors#upstream-unavailable')", "wait": { "cause": "Pipeline stopped at: adjudication_failed", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_needs_input.json b/tests/fixtures/parity/expected/verify__status_needs_input.json index 13a266b..0a1ff14 100644 --- a/tests/fixtures/parity/expected/verify__status_needs_input.json +++ b/tests/fixtures/parity/expected/verify__status_needs_input.json @@ -31,8 +31,25 @@ "status": "needs_input", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "claims": [ + { + "domain": "Science", + "text": "The Earth is round." + }, + { + "domain": "Science", + "text": "Water boils at 100C at sea level." + } + ], + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "reason": "multi_claim", + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "needs input (multi_claim)\nclaims found:\n 1. The Earth is round.\n 2. Water boils at 100C at sea level.\nresolve it: lenz verify --resume t --claim --detach\n", "render_json": "{\n \"status\": \"needs_input\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"multi_claim\",\n \"hint\": \"The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [\n {\n \"text\": \"The Earth is round.\",\n \"domain\": \"Science\"\n },\n {\n \"text\": \"Water boils at 100C at sea level.\",\n \"domain\": \"Science\"\n }\n ],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "repr": "TaskStatus(status='needs_input', task_id='7cff18da2d979d6370e54a160be121ad', reason='multi_claim', hint='The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[CandidateClaim(text='The Earth is round.', domain='Science'), CandidateClaim(text='Water boils at 100C at sea level.', domain='Science')], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "cause": "The verification needs caller input to proceed.", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_not_a_claim.json b/tests/fixtures/parity/expected/verify__status_not_a_claim.json index 5f7f1c5..1007c6e 100644 --- a/tests/fixtures/parity/expected/verify__status_not_a_claim.json +++ b/tests/fixtures/parity/expected/verify__status_not_a_claim.json @@ -22,8 +22,19 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "error": "Not a verifiable claim.", + "failure_class": "invalid_input", + "failure_reason": "not_a_claim", + "hint": "The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims.", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — Not a verifiable claim.\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims.\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Not a verifiable claim.\",\n \"failure_reason\": \"not_a_claim\",\n \"failure_detail\": \"\",\n \"failure_class\": \"invalid_input\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#invalid-input\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims.', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Not a verifiable claim.', failure_reason='not_a_claim', failure_detail='', failure_class='invalid_input', retryable=False, docs_url='https://lenz.io/docs/errors#invalid-input')", "wait": { "cause": "Not a verifiable claim.", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_not_a_claim_after_a_while.json b/tests/fixtures/parity/expected/verify__status_not_a_claim_after_a_while.json index 422b2c4..d056d07 100644 --- a/tests/fixtures/parity/expected/verify__status_not_a_claim_after_a_while.json +++ b/tests/fixtures/parity/expected/verify__status_not_a_claim_after_a_while.json @@ -22,8 +22,18 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#invalid-input", + "error": "Pipeline stopped: not_a_claim.", + "failure_class": "invalid_input", + "failure_reason": "not_a_claim", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — Pipeline stopped: not_a_claim.\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped: not_a_claim.\",\n \"failure_reason\": \"not_a_claim\",\n \"failure_detail\": \"\",\n \"failure_class\": \"invalid_input\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#invalid-input\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Pipeline stopped: not_a_claim.', failure_reason='not_a_claim', failure_detail='', failure_class='invalid_input', retryable=False, docs_url='https://lenz.io/docs/errors#invalid-input')", "wait": { "cause": "Pipeline stopped: not_a_claim.", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_older_run_completed.json b/tests/fixtures/parity/expected/verify__status_older_run_completed.json index 4455d97..9f88760 100644 --- a/tests/fixtures/parity/expected/verify__status_older_run_completed.json +++ b/tests/fixtures/parity/expected/verify__status_older_run_completed.json @@ -64,7 +64,7 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -97,7 +97,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" @@ -108,9 +108,101 @@ "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render": "True (high) score 9/10\n\nverification_id: 4d44dfaf\nfull report: lenz show 4d44dfaf\n", - "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"4d44dfaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:21:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", - "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 4d44dfaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 4d44dfaf \"\"\n", + "dump_unset": { + "result": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "True (high) score 9/10\n\nverification_id: 68ca4aaf\nfull report: lenz show 68ca4aaf\n", + "render_json": "{\n \"status\": \"completed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": {\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n },\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_verification": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", + "repr": "TaskStatus(status='completed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1')), claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "returned": { "audit": { @@ -160,7 +252,7 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -193,7 +285,7 @@ ], "suggested_rewrite": null, "verdict": "True", - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "visibility": "private", "warnings": [ "Relies on limited sources" diff --git a/tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json b/tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json index 6700b6d..04bfe69 100644 --- a/tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json +++ b/tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json @@ -22,8 +22,18 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#internal", + "error": "Pipeline failed.", + "failure_class": "internal", + "failure_reason": "task_error", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — Pipeline failed.\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline failed.\",\n \"failure_reason\": \"task_error\",\n \"failure_detail\": \"\",\n \"failure_class\": \"internal\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#internal\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Pipeline failed.', failure_reason='task_error', failure_detail='', failure_class='internal', retryable=False, docs_url='https://lenz.io/docs/errors#internal')", "wait": { "cause": "Pipeline failed.", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json b/tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json index c379a8d..c550bd3 100644 --- a/tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json +++ b/tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json @@ -22,8 +22,18 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#insufficient-evidence", + "error": "Pipeline stopped at: research_empty", + "failure_class": "insufficient_evidence", + "failure_reason": "research_empty", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — Pipeline stopped at: research_empty\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped at: research_empty\",\n \"failure_reason\": \"research_empty\",\n \"failure_detail\": \"\",\n \"failure_class\": \"insufficient_evidence\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#insufficient-evidence\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Pipeline stopped at: research_empty', failure_reason='research_empty', failure_detail='', failure_class='insufficient_evidence', retryable=False, docs_url='https://lenz.io/docs/errors#insufficient-evidence')", "wait": { "cause": "Pipeline stopped at: research_empty", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_older_run_in_progress.json b/tests/fixtures/parity/expected/verify__status_older_run_in_progress.json index fb3142e..01a834c 100644 --- a/tests/fixtures/parity/expected/verify__status_older_run_in_progress.json +++ b/tests/fixtures/parity/expected/verify__status_older_run_in_progress.json @@ -9,7 +9,7 @@ "failure_reason": "", "hint": "", "progress": { - "elapsed_seconds": 2, + "elapsed_seconds": 1, "index": 2, "poll_after_seconds": 5, "step": "research", @@ -22,8 +22,20 @@ "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "progress": { + "elapsed_seconds": 1, + "index": 2, + "poll_after_seconds": 5, + "step": "research", + "total": 5 + }, + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "processing — Gathering evidence (2/5)\n", - "render_json": "{\n \"status\": \"processing\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"research\",\n \"index\": 2,\n \"total\": 5,\n \"elapsed_seconds\": 2,\n \"poll_after_seconds\": 5\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_json": "{\n \"status\": \"processing\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"research\",\n \"index\": 2,\n \"total\": 5,\n \"elapsed_seconds\": 1,\n \"poll_after_seconds\": 5\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "repr": "TaskStatus(status='processing', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='research', index=2, total=5, elapsed_seconds=1, poll_after_seconds=5), result=None, claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "cause": "unknown", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_processing.json b/tests/fixtures/parity/expected/verify__status_processing.json index fb3142e..01a834c 100644 --- a/tests/fixtures/parity/expected/verify__status_processing.json +++ b/tests/fixtures/parity/expected/verify__status_processing.json @@ -9,7 +9,7 @@ "failure_reason": "", "hint": "", "progress": { - "elapsed_seconds": 2, + "elapsed_seconds": 1, "index": 2, "poll_after_seconds": 5, "step": "research", @@ -22,8 +22,20 @@ "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "progress": { + "elapsed_seconds": 1, + "index": 2, + "poll_after_seconds": 5, + "step": "research", + "total": 5 + }, + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "processing — Gathering evidence (2/5)\n", - "render_json": "{\n \"status\": \"processing\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"research\",\n \"index\": 2,\n \"total\": 5,\n \"elapsed_seconds\": 2,\n \"poll_after_seconds\": 5\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_json": "{\n \"status\": \"processing\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"research\",\n \"index\": 2,\n \"total\": 5,\n \"elapsed_seconds\": 1,\n \"poll_after_seconds\": 5\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "repr": "TaskStatus(status='processing', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='research', index=2, total=5, elapsed_seconds=1, poll_after_seconds=5), result=None, claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "cause": "unknown", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_processing_after_a_while.json b/tests/fixtures/parity/expected/verify__status_processing_after_a_while.json index e7bb01d..03a199c 100644 --- a/tests/fixtures/parity/expected/verify__status_processing_after_a_while.json +++ b/tests/fixtures/parity/expected/verify__status_processing_after_a_while.json @@ -9,7 +9,7 @@ "failure_reason": "", "hint": "", "progress": { - "elapsed_seconds": 2, + "elapsed_seconds": 1, "index": 4, "poll_after_seconds": 5, "step": "adjudication", @@ -22,8 +22,20 @@ "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "progress": { + "elapsed_seconds": 1, + "index": 4, + "poll_after_seconds": 5, + "step": "adjudication", + "total": 5 + }, + "status": "processing", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "processing — Panel review across models (4/5)\n", - "render_json": "{\n \"status\": \"processing\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"adjudication\",\n \"index\": 4,\n \"total\": 5,\n \"elapsed_seconds\": 2,\n \"poll_after_seconds\": 5\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "render_json": "{\n \"status\": \"processing\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"adjudication\",\n \"index\": 4,\n \"total\": 5,\n \"elapsed_seconds\": 1,\n \"poll_after_seconds\": 5\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "repr": "TaskStatus(status='processing', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='adjudication', index=4, total=5, elapsed_seconds=1, poll_after_seconds=5), result=None, claims=[], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", "wait": { "cause": "unknown", "code": "", diff --git a/tests/fixtures/parity/expected/verify__status_task_stuck.json b/tests/fixtures/parity/expected/verify__status_task_stuck.json index d7e9b4b..db68477 100644 --- a/tests/fixtures/parity/expected/verify__status_task_stuck.json +++ b/tests/fixtures/parity/expected/verify__status_task_stuck.json @@ -22,8 +22,18 @@ "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad" }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#upstream-unavailable", + "error": "The task was never completed and has been marked failed.", + "failure_class": "upstream_unavailable", + "failure_reason": "task_stuck", + "retryable": true, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "render": "failed — The task was never completed and has been marked failed.\n", "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"The task was never completed and has been marked failed.\",\n \"failure_reason\": \"task_stuck\",\n \"failure_detail\": \"\",\n \"failure_class\": \"upstream_unavailable\",\n \"retryable\": true,\n \"docs_url\": \"https://lenz.io/docs/errors#upstream-unavailable\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='The task was never completed and has been marked failed.', failure_reason='task_stuck', failure_detail='', failure_class='upstream_unavailable', retryable=True, docs_url='https://lenz.io/docs/errors#upstream-unavailable')", "wait": { "cause": "The task was never completed and has been marked failed.", "code": "", diff --git a/tests/fixtures/parity/expected/verify__submit_202.json b/tests/fixtures/parity/expected/verify__submit_202.json index 4aa7f3a..74a0269 100644 --- a/tests/fixtures/parity/expected/verify__submit_202.json +++ b/tests/fixtures/parity/expected/verify__submit_202.json @@ -1,9 +1,15 @@ { "dump": { - "chain_id": "78ac4e9f234ea3bd", + "chain_id": "99a0a9ca89e1e72c", "claim_text": "", "status": "queued", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render_json": null + "dump_unset": { + "chain_id": "99a0a9ca89e1e72c", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null, + "repr": "TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='', status='queued', chain_id='99a0a9ca89e1e72c')" } diff --git a/tests/fixtures/parity/expected/verify__submit_202_options.json b/tests/fixtures/parity/expected/verify__submit_202_options.json index 4aa7f3a..74a0269 100644 --- a/tests/fixtures/parity/expected/verify__submit_202_options.json +++ b/tests/fixtures/parity/expected/verify__submit_202_options.json @@ -1,9 +1,15 @@ { "dump": { - "chain_id": "78ac4e9f234ea3bd", + "chain_id": "99a0a9ca89e1e72c", "claim_text": "", "status": "queued", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render_json": null + "dump_unset": { + "chain_id": "99a0a9ca89e1e72c", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null, + "repr": "TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='', status='queued', chain_id='99a0a9ca89e1e72c')" } diff --git a/tests/fixtures/parity/expected/verify__submit_202_text_alias.json b/tests/fixtures/parity/expected/verify__submit_202_text_alias.json index 4aa7f3a..74a0269 100644 --- a/tests/fixtures/parity/expected/verify__submit_202_text_alias.json +++ b/tests/fixtures/parity/expected/verify__submit_202_text_alias.json @@ -1,9 +1,15 @@ { "dump": { - "chain_id": "78ac4e9f234ea3bd", + "chain_id": "99a0a9ca89e1e72c", "claim_text": "", "status": "queued", "task_id": "7cff18da2d979d6370e54a160be121ad" }, - "render_json": null + "dump_unset": { + "chain_id": "99a0a9ca89e1e72c", + "status": "queued", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render_json": null, + "repr": "TaskAccepted(task_id='7cff18da2d979d6370e54a160be121ad', claim_text='', status='queued', chain_id='99a0a9ca89e1e72c')" } diff --git a/tests/fixtures/parity/expected/verify__verification_200.json b/tests/fixtures/parity/expected/verify__verification_200.json index 9c9202f..8ec0e7a 100644 --- a/tests/fixtures/parity/expected/verify__verification_200.json +++ b/tests/fixtures/parity/expected/verify__verification_200.json @@ -47,7 +47,94 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -88,5 +175,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_200_covered.json b/tests/fixtures/parity/expected/verify__verification_200_covered.json index 9e542b8..ed0c52c 100644 --- a/tests/fixtures/parity/expected/verify__verification_200_covered.json +++ b/tests/fixtures/parity/expected/verify__verification_200_covered.json @@ -38,14 +38,99 @@ "aggregate": 500000, "as_of": "2026-03-10T12:01:30+00:00", "cap": 10000, - "certificate_id": "def7232d27eda03cf261569aee9948c5", - "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", "currency": "EUR", "reasons": [], "status": "covered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": "2026-03-10T12:01:30+00:00", + "cap": 10000, + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", + "currency": "EUR", + "reasons": [], + "status": "covered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -86,5 +171,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"covered\",\n \"reasons\": [],\n \"certificate_id\": \"def7232d27eda03cf261569aee9948c5\",\n \"certificate_url\": \"https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5\",\n \"as_of\": \"2026-03-10T12:01:30+00:00\",\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"covered\",\n \"reasons\": [],\n \"certificate_id\": \"95b0f3dd008b46846b6dd6b15099e2b3\",\n \"certificate_url\": \"https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3\",\n \"as_of\": \"2026-03-10T12:01:30+00:00\",\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='covered', reasons=[], certificate_id='95b0f3dd008b46846b6dd6b15099e2b3', certificate_url='https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3', as_of='2026-03-10T12:01:30+00:00', currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/verify__verification_200_modified_at_crosses_midnight_by_minutes.json index fc049f4..ab23f79 100644 --- a/tests/fixtures/parity/expected/verify__verification_200_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/expected/verify__verification_200_modified_at_crosses_midnight_by_minutes.json @@ -86,7 +86,95 @@ "Relies on limited sources" ] }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T23:55:00.123456+00:00', modified_at='2026-10-02T00:03:00.123456+00:00', language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_200_modified_at_null.json b/tests/fixtures/parity/expected/verify__verification_200_modified_at_null.json index 9c9202f..8ec0e7a 100644 --- a/tests/fixtures/parity/expected/verify__verification_200_modified_at_null.json +++ b/tests/fixtures/parity/expected/verify__verification_200_modified_at_null.json @@ -47,7 +47,94 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -88,5 +175,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/verify__verification_200_modified_at_same_day_hours_apart.json index 11aaf1c..6e45445 100644 --- a/tests/fixtures/parity/expected/verify__verification_200_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/expected/verify__verification_200_modified_at_same_day_hours_apart.json @@ -86,7 +86,95 @@ "Relies on limited sources" ] }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T08:00:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T08:00:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T08:00:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T08:00:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_200_modified_at_set.json b/tests/fixtures/parity/expected/verify__verification_200_modified_at_set.json index fc049f4..ab23f79 100644 --- a/tests/fixtures/parity/expected/verify__verification_200_modified_at_set.json +++ b/tests/fixtures/parity/expected/verify__verification_200_modified_at_set.json @@ -86,7 +86,95 @@ "Relies on limited sources" ] }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T23:55:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": "2026-10-02T00:03:00.123456+00:00", + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T23:55:00.123456+00:00\",\n \"modified_at\": \"2026-10-02T00:03:00.123456+00:00\",\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T23:55:00.123456+00:00', modified_at='2026-10-02T00:03:00.123456+00:00', language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_200_result_json_conclusion_only.json b/tests/fixtures/parity/expected/verify__verification_200_result_json_conclusion_only.json index 7df6e17..30e90b7 100644 --- a/tests/fixtures/parity/expected/verify__verification_200_result_json_conclusion_only.json +++ b/tests/fixtures/parity/expected/verify__verification_200_result_json_conclusion_only.json @@ -30,7 +30,55 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "", + "sources": [], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_con": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "debate_pro": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "panel_agreement": "split" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [], @@ -49,5 +97,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\n── Panel ──\nagreement: split\n\nDebate\n PRO \n CON \n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [],\n \"presumed_intent\": \"\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [],\n \"suggested_rewrite\": null,\n \"sources\": [],\n \"audit\": {\n \"adjudication_summary\": \"\",\n \"assessments\": [],\n \"debate_pro\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"debate_con\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"panel_agreement\": \"split\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [],\n \"presumed_intent\": \"\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [],\n \"suggested_rewrite\": null,\n \"sources\": [],\n \"audit\": {\n \"adjudication_summary\": \"\",\n \"assessments\": [],\n \"debate_pro\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"debate_con\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"panel_agreement\": \"split\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[], presumed_intent='', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=[], suggested_rewrite=None, sources=[], audit=Audit(adjudication_summary='', assessments=[], debate_pro=DebateSide(role='', argument='', rebuttal=''), debate_con=DebateSide(role='', argument='', rebuttal=''), panel_agreement='split'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_200_result_json_empty.json b/tests/fixtures/parity/expected/verify__verification_200_result_json_empty.json index 7df6e17..30e90b7 100644 --- a/tests/fixtures/parity/expected/verify__verification_200_result_json_empty.json +++ b/tests/fixtures/parity/expected/verify__verification_200_result_json_empty.json @@ -30,7 +30,55 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "", + "sources": [], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_con": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "debate_pro": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "panel_agreement": "split" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [], @@ -49,5 +97,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\n── Panel ──\nagreement: split\n\nDebate\n PRO \n CON \n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [],\n \"presumed_intent\": \"\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [],\n \"suggested_rewrite\": null,\n \"sources\": [],\n \"audit\": {\n \"adjudication_summary\": \"\",\n \"assessments\": [],\n \"debate_pro\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"debate_con\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"panel_agreement\": \"split\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [],\n \"presumed_intent\": \"\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [],\n \"suggested_rewrite\": null,\n \"sources\": [],\n \"audit\": {\n \"adjudication_summary\": \"\",\n \"assessments\": [],\n \"debate_pro\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"debate_con\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"panel_agreement\": \"split\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[], presumed_intent='', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=[], suggested_rewrite=None, sources=[], audit=Audit(adjudication_summary='', assessments=[], debate_pro=DebateSide(role='', argument='', rebuttal=''), debate_con=DebateSide(role='', argument='', rebuttal=''), panel_agreement='split'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_200_result_json_old_shape_strings.json b/tests/fixtures/parity/expected/verify__verification_200_result_json_old_shape_strings.json index a8398f8..f9f875f 100644 --- a/tests/fixtures/parity/expected/verify__verification_200_result_json_old_shape_strings.json +++ b/tests/fixtures/parity/expected/verify__verification_200_result_json_old_shape_strings.json @@ -38,7 +38,71 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "", + "sources": [ + { + "date": "", + "snippet": "Old row.", + "source_name": "", + "title": "", + "url": "https://old.example/a" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "", + "assessments": [ + { + "focus_area": "", + "panelist_name": "Reviewer A", + "reasoning": "", + "score": null, + "warnings": [] + } + ], + "debate_con": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "debate_pro": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "panel_agreement": "split" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [], @@ -65,5 +129,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (1):\n • https://old.example/a\n https://old.example/a\n Old row.\n\n── Panel ──\nagreement: split\n\nReviewer A\n\nDebate\n PRO \n CON \n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (1):\n • https://old.example/a\n https://old.example/a\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [],\n \"presumed_intent\": \"\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"\",\n \"title\": \"\",\n \"url\": \"https://old.example/a\",\n \"snippet\": \"Old row.\",\n \"date\": \"\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"\",\n \"score\": null,\n \"reasoning\": \"\",\n \"warnings\": []\n }\n ],\n \"debate_pro\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"debate_con\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"panel_agreement\": \"split\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [],\n \"presumed_intent\": \"\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"\",\n \"title\": \"\",\n \"url\": \"https://old.example/a\",\n \"snippet\": \"Old row.\",\n \"date\": \"\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"\",\n \"score\": null,\n \"reasoning\": \"\",\n \"warnings\": []\n }\n ],\n \"debate_pro\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"debate_con\": {\n \"role\": \"\",\n \"argument\": \"\",\n \"rebuttal\": \"\"\n },\n \"panel_agreement\": \"split\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[], presumed_intent='', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=[], suggested_rewrite=None, sources=[Source(source_name='', title='', url='https://old.example/a', snippet='Old row.', date='')], audit=Audit(adjudication_summary='', assessments=[Assessment(panelist_name='Reviewer A', focus_area='', score=None, reasoning='', warnings=[])], debate_pro=DebateSide(role='', argument='', rebuttal=''), debate_con=DebateSide(role='', argument='', rebuttal=''), panel_agreement='split'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_by_task_id_200.json b/tests/fixtures/parity/expected/verify__verification_by_task_id_200.json index 9c9202f..8ec0e7a 100644 --- a/tests/fixtures/parity/expected/verify__verification_by_task_id_200.json +++ b/tests/fixtures/parity/expected/verify__verification_by_task_id_200.json @@ -47,7 +47,94 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -88,5 +175,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_public_anonymous.json b/tests/fixtures/parity/expected/verify__verification_public_anonymous.json index ab58772..7319f6f 100644 --- a/tests/fixtures/parity/expected/verify__verification_public_anonymous.json +++ b/tests/fixtures/parity/expected/verify__verification_public_anonymous.json @@ -35,7 +35,82 @@ "claim": "The Earth is round.", "confidence": "high", "coverage": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "public", + "warnings": [ + "Relies on limited sources" + ] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": null, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -76,5 +151,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"public\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": null\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"public\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": null\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='public', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=None)" } diff --git a/tests/fixtures/parity/expected/verify__verification_public_of_another_user.json b/tests/fixtures/parity/expected/verify__verification_public_of_another_user.json index d21e90e..dc75e23 100644 --- a/tests/fixtures/parity/expected/verify__verification_public_of_another_user.json +++ b/tests/fixtures/parity/expected/verify__verification_public_of_another_user.json @@ -47,7 +47,94 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is verified.", + "key_finding": "The Earth is approximately spherical in shape.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "public", + "warnings": [ + "Relies on limited sources" + ] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -88,5 +175,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical in shape.\n\nThe claim is verified.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"public\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"public\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical in shape.\",\n \"executive_summary\": \"The claim is verified.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='public', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical in shape.', executive_summary='The claim is verified.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__verification_zero_retention_served.json b/tests/fixtures/parity/expected/verify__verification_zero_retention_served.json index e12b093..463876f 100644 --- a/tests/fixtures/parity/expected/verify__verification_zero_retention_served.json +++ b/tests/fixtures/parity/expected/verify__verification_zero_retention_served.json @@ -47,7 +47,94 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", + "depth": "standard", + "domain": "Science", + "entities": [ + { + "name": "Earth", + "qid": null + } + ], + "executive_summary": "The claim is well supported by evidence.", + "key_finding": "The Earth is approximately spherical.", + "language": "en", + "lenz_score": 9, + "modified_at": null, + "presumed_intent": "Verify a basic scientific fact", + "sources": [ + { + "date": "2025-01-15", + "snippet": "Satellite imagery shows an oblate spheroid.", + "source_name": "NASA", + "title": "The shape of the Earth", + "url": "https://nasa.example/shape" + }, + { + "date": "2025-01-16", + "snippet": "Orbital measurements confirm the curvature.", + "source_name": "ESA", + "title": "Earth from space", + "url": "https://esa.example/earth" + } + ], + "suggested_rewrite": null, + "verdict": "True", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [ + "Relies on limited sources" + ] + }, + "dump_unset": { + "audit": { + "adjudication_summary": "Both reviewers agreed.", + "assessments": [ + { + "focus_area": "Claim Precision & Quantitative Accuracy", + "panelist_name": "Reviewer A", + "reasoning": "Precision analysis reasoning.", + "score": 9.0, + "warnings": [] + }, + { + "focus_area": "Sources", + "panelist_name": "Reviewer B", + "reasoning": "Source audit reasoning.", + "score": 9.0, + "warnings": [ + "weak source A" + ] + } + ], + "debate_con": { + "argument": "The Earth is not a perfect sphere.", + "rebuttal": "Oblate, but round for the claim.", + "role": "Skeptic" + }, + "debate_pro": { + "argument": "Satellite imagery and geodesy agree.", + "rebuttal": "Flat-Earth objections do not hold.", + "role": "Advocate" + }, + "panel_agreement": "unanimous" + }, + "claim": "The Earth is round.", + "confidence": "high", + "coverage": { + "aggregate": 500000, + "as_of": null, + "cap": 10000, + "certificate_id": null, + "certificate_url": null, + "currency": "EUR", + "reasons": [ + "plan" + ], + "status": "uncovered", + "terms_version": "v1" + }, + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Science", "entities": [ @@ -88,5 +175,6 @@ }, "render": "True (high) score 9/10\n\nClaim The Earth is round.\nScience • Earth\n\nThe Earth is approximately spherical.\n\nThe claim is well supported by evidence.\n\nWarnings (1):\n • Relies on limited sources\n\nSources (2):\n • The shape of the Earth (2025-01-15)\n https://nasa.example/shape\n Satellite imagery shows an oblate spheroid.\n • Earth from space (2025-01-16)\n https://esa.example/earth\n Orbital measurements confirm the curvature.\n\n── Panel ──\nagreement: unanimous\n\nReviewer A (Claim Precision & Quantitative Accuracy) 9.0/10\n Precision analysis reasoning.\n\nReviewer B (Sources) 9.0/10\n Source audit reasoning.\n • weak source A\n\nPanel Review\nBoth reviewers agreed.\n\nDebate\n PRO Satellite imagery and geodesy agree.\n rebuttal: Flat-Earth objections do not hold.\n CON The Earth is not a perfect sphere.\n rebuttal: Oblate, but round for the claim.\n\nverification_id: 68ca4aaf\nchecked Oct 1, 2026\nask follow-ups: lenz ask 68ca4aaf \"\"\n", "render_concise": "True (high) score 9/10\n\nThe Earth is approximately spherical.\n\nThe claim is well supported by evidence.\n\nSources (2):\n • The shape of the Earth\n https://nasa.example/shape\n • Earth from space\n https://esa.example/earth\n\nverification_id: 68ca4aaf\nask follow-ups: lenz ask 68ca4aaf \"\"\n", - "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical.\",\n \"executive_summary\": \"The claim is well supported by evidence.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:14:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n" + "render_json": "{\n \"verification_id\": \"68ca4aaf\",\n \"claim\": \"The Earth is round.\",\n \"visibility\": \"private\",\n \"depth\": \"standard\",\n \"domain\": \"Science\",\n \"entities\": [\n {\n \"name\": \"Earth\",\n \"qid\": null\n }\n ],\n \"presumed_intent\": \"Verify a basic scientific fact\",\n \"verdict\": \"True\",\n \"confidence\": \"high\",\n \"lenz_score\": 9,\n \"key_finding\": \"The Earth is approximately spherical.\",\n \"executive_summary\": \"The claim is well supported by evidence.\",\n \"warnings\": [\n \"Relies on limited sources\"\n ],\n \"suggested_rewrite\": null,\n \"sources\": [\n {\n \"source_name\": \"NASA\",\n \"title\": \"The shape of the Earth\",\n \"url\": \"https://nasa.example/shape\",\n \"snippet\": \"Satellite imagery shows an oblate spheroid.\",\n \"date\": \"2025-01-15\"\n },\n {\n \"source_name\": \"ESA\",\n \"title\": \"Earth from space\",\n \"url\": \"https://esa.example/earth\",\n \"snippet\": \"Orbital measurements confirm the curvature.\",\n \"date\": \"2025-01-16\"\n }\n ],\n \"audit\": {\n \"adjudication_summary\": \"Both reviewers agreed.\",\n \"assessments\": [\n {\n \"panelist_name\": \"Reviewer A\",\n \"focus_area\": \"Claim Precision & Quantitative Accuracy\",\n \"score\": 9.0,\n \"reasoning\": \"Precision analysis reasoning.\",\n \"warnings\": []\n },\n {\n \"panelist_name\": \"Reviewer B\",\n \"focus_area\": \"Sources\",\n \"score\": 9.0,\n \"reasoning\": \"Source audit reasoning.\",\n \"warnings\": [\n \"weak source A\"\n ]\n }\n ],\n \"debate_pro\": {\n \"role\": \"Advocate\",\n \"argument\": \"Satellite imagery and geodesy agree.\",\n \"rebuttal\": \"Flat-Earth objections do not hold.\"\n },\n \"debate_con\": {\n \"role\": \"Skeptic\",\n \"argument\": \"The Earth is not a perfect sphere.\",\n \"rebuttal\": \"Oblate, but round for the claim.\"\n },\n \"panel_agreement\": \"unanimous\"\n },\n \"created_at\": \"2026-10-01T09:07:00.123456+00:00\",\n \"modified_at\": null,\n \"language\": \"en\",\n \"coverage\": {\n \"status\": \"uncovered\",\n \"reasons\": [\n \"plan\"\n ],\n \"certificate_id\": null,\n \"certificate_url\": null,\n \"as_of\": null,\n \"currency\": \"EUR\",\n \"cap\": 10000,\n \"aggregate\": 500000,\n \"terms_version\": \"v1\"\n }\n}\n", + "repr": "Verification(verification_id='68ca4aaf', claim='The Earth is round.', visibility='private', depth='standard', domain='Science', entities=[EntityRef(name='Earth', qid=None)], presumed_intent='Verify a basic scientific fact', verdict='True', confidence='high', lenz_score=9, key_finding='The Earth is approximately spherical.', executive_summary='The claim is well supported by evidence.', warnings=['Relies on limited sources'], suggested_rewrite=None, sources=[Source(source_name='NASA', title='The shape of the Earth', url='https://nasa.example/shape', snippet='Satellite imagery shows an oblate spheroid.', date='2025-01-15'), Source(source_name='ESA', title='Earth from space', url='https://esa.example/earth', snippet='Orbital measurements confirm the curvature.', date='2025-01-16')], audit=Audit(adjudication_summary='Both reviewers agreed.', assessments=[Assessment(panelist_name='Reviewer A', focus_area='Claim Precision & Quantitative Accuracy', score=9.0, reasoning='Precision analysis reasoning.', warnings=[]), Assessment(panelist_name='Reviewer B', focus_area='Sources', score=9.0, reasoning='Source audit reasoning.', warnings=['weak source A'])], debate_pro=DebateSide(role='Advocate', argument='Satellite imagery and geodesy agree.', rebuttal='Flat-Earth objections do not hold.'), debate_con=DebateSide(role='Skeptic', argument='The Earth is not a perfect sphere.', rebuttal='Oblate, but round for the claim.'), panel_agreement='unanimous'), created_at='2026-10-01T09:07:00.123456+00:00', modified_at=None, language='en', coverage=Coverage(status='uncovered', reasons=['plan'], certificate_id=None, certificate_url=None, as_of=None, currency='EUR', cap=10000, aggregate=500000, terms_version='v1'))" } diff --git a/tests/fixtures/parity/expected/verify__webhook_secret_missing_422.json b/tests/fixtures/parity/expected/verify__webhook_secret_missing_422.json new file mode 100644 index 0000000..aa4b7e1 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__webhook_secret_missing_422.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "webhook_url was supplied but this API key has no HMAC secret. Generate one at https://lenz.io/api-credentials (Webhooks panel) before submitting webhook-delivered calls.", + "code": "webhook_secret_missing", + "doc_url": "https://lenz.io/docs/errors/validation", + "errors": [], + "fix": "Check the request body against the OpenAPI spec.", + "friendly_text": "webhook_url was supplied but this API key has no HMAC secret. Generate one at https://lenz.io/api-credentials (Webhooks panel) before submitting webhook-delivered calls.\n Fix: Check the request body against the OpenAPI spec.", + "message": "webhook_url was supplied but this API key has no HMAC secret. Generate one at https://lenz.io/api-credentials (Webhooks panel) before submitting webhook-delivered calls.", + "payload_json": { + "error": { + "code": "invalid_request", + "fix": "Check the request body against the OpenAPI spec.", + "message": "webhook_url was supplied but this API key has no HMAC secret. Generate one at https://lenz.io/api-credentials (Webhooks panel) before submitting webhook-delivered calls.", + "status": 422 + } + }, + "request_id": "", + "status_code": 422, + "type": "LenzValidationError" + } +} diff --git a/tests/fixtures/parity/expected/webhook__certificate_timestamped.json b/tests/fixtures/parity/expected/webhook__certificate_timestamped.json index 49e54c1..754e890 100644 --- a/tests/fixtures/parity/expected/webhook__certificate_timestamped.json +++ b/tests/fixtures/parity/expected/webhook__certificate_timestamped.json @@ -6,17 +6,17 @@ "aggregate": 500000, "as_of": "2026-09-03T10:00:00+00:00", "cap": 10000, - "certificate_id": "def7232d27eda03cf261569aee9948c5", - "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", "currency": "EUR", "reasons": [], "status": "covered", "terms_version": "v1" }, - "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "certificate.timestamped", "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "CertificateTimestamped", "verification_id": "68ca4aaf" } diff --git a/tests/fixtures/parity/expected/webhook__certificate_timestamped_review_child.json b/tests/fixtures/parity/expected/webhook__certificate_timestamped_review_child.json index 49e54c1..754e890 100644 --- a/tests/fixtures/parity/expected/webhook__certificate_timestamped_review_child.json +++ b/tests/fixtures/parity/expected/webhook__certificate_timestamped_review_child.json @@ -6,17 +6,17 @@ "aggregate": 500000, "as_of": "2026-09-03T10:00:00+00:00", "cap": 10000, - "certificate_id": "def7232d27eda03cf261569aee9948c5", - "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", "currency": "EUR", "reasons": [], "status": "covered", "terms_version": "v1" }, - "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "certificate.timestamped", "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "CertificateTimestamped", "verification_id": "68ca4aaf" } diff --git a/tests/fixtures/parity/expected/webhook__citecheck_completed.json b/tests/fixtures/parity/expected/webhook__citecheck_completed.json index 7044240..0fdf121 100644 --- a/tests/fixtures/parity/expected/webhook__citecheck_completed.json +++ b/tests/fixtures/parity/expected/webhook__citecheck_completed.json @@ -95,9 +95,9 @@ "statement": "The agency said so in a statement." } ], - "citecheck_id": "98c0e7d5", - "completed_at": "2026-10-01T09:35:00.123456Z", - "created_at": "2026-10-01T09:28:00.123456Z", + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 2 }, @@ -123,12 +123,12 @@ "citations_selected": 2 } }, - "citecheck_id": "98c0e7d5", - "delivered_at": "2026-10-01T09:42:00.123456+00:00", + "citecheck_id": "8eb49302", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "citecheck.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", "status": "completed", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "CitecheckEvent", "verification_id": null } diff --git a/tests/fixtures/parity/expected/webhook__citecheck_failed.json b/tests/fixtures/parity/expected/webhook__citecheck_failed.json index aaa4c74..400e0d6 100644 --- a/tests/fixtures/parity/expected/webhook__citecheck_failed.json +++ b/tests/fixtures/parity/expected/webhook__citecheck_failed.json @@ -6,9 +6,9 @@ "citation_failures": [], "citation_issues": [], "citations": [], - "citecheck_id": "98c0e7d5", - "completed_at": "2026-10-01T09:35:00.123456Z", - "created_at": "2026-10-01T09:28:00.123456Z", + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -36,12 +36,12 @@ "citations_selected": null } }, - "citecheck_id": "98c0e7d5", - "delivered_at": "2026-10-01T09:42:00.123456+00:00", + "citecheck_id": "8eb49302", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "citecheck.failed", "event_id": "evt_09301870aba4e8f6ae33ff60", "status": "failed", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "CitecheckEvent", "verification_id": null } diff --git a/tests/fixtures/parity/expected/webhook__review_completed.json b/tests/fixtures/parity/expected/webhook__review_completed.json index 4e39f6b..e1f3697 100644 --- a/tests/fixtures/parity/expected/webhook__review_completed.json +++ b/tests/fixtures/parity/expected/webhook__review_completed.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T10:03:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "review.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", "review": { @@ -44,7 +44,7 @@ "claim": "The Eiffel Tower is in Berlin.", "confidence": "high", "content_status": "available", - "created_at": "2026-10-01T09:56:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "depth": "standard", "domain": "", "entities": [], @@ -56,11 +56,11 @@ "modified_at": null, "status": "completed", "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "task_id": "5ccac77f53ff49328ec134fb29d66542", - "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "8ceed6f0", - "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", + "verification_id": "68ca4aaf", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "visibility": "private", "warnings": [ "w1" @@ -68,8 +68,8 @@ } } ], - "completed_at": "2026-10-01T09:35:00.123456Z", - "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 11 }, @@ -92,11 +92,11 @@ "source": "verification", "suggested_edits": null, "suggested_rewrite": "Fixed: The Eiffel Tower is in Berlin.", - "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "verdict": "False", - "verification_id": "8ceed6f0", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", "verified_claim": null } ], @@ -121,7 +121,7 @@ ] }, "poll_after_seconds": null, - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "completed", "summary": { "assessments": { @@ -148,9 +148,9 @@ }, "view": "full" }, - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "completed", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "ReviewEvent", "verification_id": null } diff --git a/tests/fixtures/parity/expected/webhook__review_completed_key_default_url.json b/tests/fixtures/parity/expected/webhook__review_completed_key_default_url.json index 61e1a22..85da702 100644 --- a/tests/fixtures/parity/expected/webhook__review_completed_key_default_url.json +++ b/tests/fixtures/parity/expected/webhook__review_completed_key_default_url.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T09:42:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "review.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", "review": { @@ -41,8 +41,8 @@ "verification": null } ], - "completed_at": "2026-10-01T09:35:00.123456Z", - "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 1 }, @@ -70,7 +70,7 @@ ] }, "poll_after_seconds": null, - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "completed", "summary": { "assessments": { @@ -97,9 +97,9 @@ }, "view": "full" }, - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "completed", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "ReviewEvent", "verification_id": null } diff --git a/tests/fixtures/parity/expected/webhook__review_completed_oversized_rebuilt.json b/tests/fixtures/parity/expected/webhook__review_completed_oversized_rebuilt.json index 9fd5c6f..048af92 100644 --- a/tests/fixtures/parity/expected/webhook__review_completed_oversized_rebuilt.json +++ b/tests/fixtures/parity/expected/webhook__review_completed_oversized_rebuilt.json @@ -6,9 +6,9 @@ "event": "review.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", "review": null, - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "completed", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "ReviewEvent", "verification_id": null } diff --git a/tests/fixtures/parity/expected/webhook__review_failed.json b/tests/fixtures/parity/expected/webhook__review_failed.json index 08af731..ce0bfb0 100644 --- a/tests/fixtures/parity/expected/webhook__review_failed.json +++ b/tests/fixtures/parity/expected/webhook__review_failed.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T09:42:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "review.failed", "event_id": "evt_09301870aba4e8f6ae33ff60", "review": { @@ -10,8 +10,8 @@ "citation_issues": [], "citations": [], "claims": [], - "completed_at": "2026-10-01T09:35:00.123456Z", - "created_at": "2026-10-01T09:28:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "credits": { "charged": 0 }, @@ -45,7 +45,7 @@ ] }, "poll_after_seconds": null, - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "failed", "summary": { "assessments": null, @@ -65,9 +65,9 @@ }, "view": "full" }, - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "status": "failed", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "ReviewEvent", "verification_id": null } diff --git a/tests/fixtures/parity/expected/webhook__test_fire.json b/tests/fixtures/parity/expected/webhook__test_fire.json new file mode 100644 index 0000000..b796dcd --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__test_fire.json @@ -0,0 +1,12 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T11:07:00.123456+00:00", + "event": "webhook.test", + "status": "test", + "task_id": "test_7cff18da2d979d63", + "type": "WebhookEvent", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_completed.json b/tests/fixtures/parity/expected/webhook__verification_completed.json index aa45bf9..1f8d807 100644 --- a/tests/fixtures/parity/expected/webhook__verification_completed.json +++ b/tests/fixtures/parity/expected/webhook__verification_completed.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "verification.completed", "result": { "audit": { @@ -52,7 +52,7 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Geography", "entities": [ @@ -96,7 +96,7 @@ ] }, "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "VerificationCompleted", "verification_id": "68ca4aaf" } diff --git a/tests/fixtures/parity/expected/webhook__verification_completed_in_batch.json b/tests/fixtures/parity/expected/webhook__verification_completed_in_batch.json index e6dd56f..2fff9b6 100644 --- a/tests/fixtures/parity/expected/webhook__verification_completed_in_batch.json +++ b/tests/fixtures/parity/expected/webhook__verification_completed_in_batch.json @@ -1,8 +1,8 @@ { "event": { "attempt": 1, - "batch_id": "4ee0c3aa1b671eaa", - "delivered_at": "2026-10-01T09:35:00.123456+00:00", + "batch_id": "7ea9cdbd4064e3cf", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "verification.completed", "result": { "audit": { @@ -52,7 +52,7 @@ "status": "uncovered", "terms_version": "v1" }, - "created_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "depth": "standard", "domain": "Geography", "entities": [ @@ -96,7 +96,7 @@ ] }, "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "VerificationCompleted", "verification_id": "68ca4aaf" } diff --git a/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json index c050dad..0edf9aa 100644 --- a/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T09:35:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "verification.completed", "result": { "audit": { @@ -96,7 +96,7 @@ ] }, "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "VerificationCompleted", "verification_id": "68ca4aaf" } diff --git a/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_same_day_hours_apart.json index 78e81e6..c573ea4 100644 --- a/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/expected/webhook__verification_completed_modified_at_same_day_hours_apart.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T09:28:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "verification.completed", "result": { "audit": { @@ -96,7 +96,7 @@ ] }, "status": "completed", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "type": "VerificationCompleted", "verification_id": "68ca4aaf" } diff --git a/tests/fixtures/parity/expected/webhook__verification_completed_sparse_result.json b/tests/fixtures/parity/expected/webhook__verification_completed_sparse_result.json new file mode 100644 index 0000000..51c0452 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_completed_sparse_result.json @@ -0,0 +1,48 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T11:07:00.123456+00:00", + "event": "verification.completed", + "result": { + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_con": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "debate_pro": { + "argument": "", + "rebuttal": "", + "role": "" + }, + "panel_agreement": "" + }, + "claim": "The Eiffel Tower is in Berlin.", + "confidence": "low", + "coverage": null, + "created_at": "", + "depth": "standard", + "domain": "", + "entities": [], + "executive_summary": "", + "key_finding": "", + "language": "en", + "lenz_score": null, + "modified_at": null, + "presumed_intent": "", + "sources": [], + "suggested_rewrite": null, + "verdict": "", + "verification_id": "68ca4aaf", + "visibility": "private", + "warnings": [] + }, + "status": "completed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "VerificationCompleted", + "verification_id": "68ca4aaf" + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_failed_error_none.json b/tests/fixtures/parity/expected/webhook__verification_failed_error_none.json new file mode 100644 index 0000000..3081f2a --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_failed_error_none.json @@ -0,0 +1,15 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T11:07:00.123456+00:00", + "error": "", + "event": "verification.failed", + "failure_class": "internal", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "VerificationFailed", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_failed_insufficient_evidence.json b/tests/fixtures/parity/expected/webhook__verification_failed_insufficient_evidence.json index 81bf11e..f455192 100644 --- a/tests/fixtures/parity/expected/webhook__verification_failed_insufficient_evidence.json +++ b/tests/fixtures/parity/expected/webhook__verification_failed_insufficient_evidence.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T09:14:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "error": "research_insufficient", "event": "verification.failed", "failure_class": "insufficient_evidence", diff --git a/tests/fixtures/parity/expected/webhook__verification_failed_not_a_claim.json b/tests/fixtures/parity/expected/webhook__verification_failed_not_a_claim.json index 15e827d..97484e4 100644 --- a/tests/fixtures/parity/expected/webhook__verification_failed_not_a_claim.json +++ b/tests/fixtures/parity/expected/webhook__verification_failed_not_a_claim.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T09:14:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "error": "not_a_claim", "event": "verification.failed", "failure_class": "invalid_input", diff --git a/tests/fixtures/parity/expected/webhook__verification_failed_upstream_unavailable.json b/tests/fixtures/parity/expected/webhook__verification_failed_upstream_unavailable.json index 6ec1bcb..cc5caa7 100644 --- a/tests/fixtures/parity/expected/webhook__verification_failed_upstream_unavailable.json +++ b/tests/fixtures/parity/expected/webhook__verification_failed_upstream_unavailable.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T09:14:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "error": "framing_failed", "event": "verification.failed", "failure_class": "upstream_unavailable", diff --git a/tests/fixtures/parity/expected/webhook__verification_needs_input_hint_none.json b/tests/fixtures/parity/expected/webhook__verification_needs_input_hint_none.json new file mode 100644 index 0000000..93f162e --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_needs_input_hint_none.json @@ -0,0 +1,23 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T11:07:00.123456+00:00", + "event": "verification.needs_input", + "hint": "", + "needs_input": { + "claims": [ + { + "domain": "", + "text": "The Eiffel Tower is in Berlin." + } + ], + "hint": null, + "reason": "multi_claim" + }, + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "VerificationNeedsInput", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_needs_input_multi_claim.json b/tests/fixtures/parity/expected/webhook__verification_needs_input_multi_claim.json index 55e2dc8..3a2f006 100644 --- a/tests/fixtures/parity/expected/webhook__verification_needs_input_multi_claim.json +++ b/tests/fixtures/parity/expected/webhook__verification_needs_input_multi_claim.json @@ -2,7 +2,7 @@ "event": { "attempt": 1, "batch_id": null, - "delivered_at": "2026-10-01T09:14:00.123456+00:00", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", "event": "verification.needs_input", "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", "needs_input": { diff --git a/tests/fixtures/parity/legacy/account__api_root.json b/tests/fixtures/parity/legacy/account__api_root.json new file mode 100644 index 0000000..592cd14 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__api_root.json @@ -0,0 +1,43 @@ +{ + "status": 200, + "body": { + "name": "Lenz Public API", + "version": "v1", + "docs_url": "https://lenz.io/api/v1/docs/", + "openapi_url": "https://lenz.io/api/v1/openapi.json", + "quickstart_url": "https://lenz.io/docs/quickstart", + "developer_portal_url": "https://lenz.io/developers", + "endpoints": { + "extract": "https://lenz.io/api/v1/extract", + "assess": "https://lenz.io/api/v1/assess", + "verify": "https://lenz.io/api/v1/verify", + "ask": "https://lenz.io/api/v1/ask/{verification_id}", + "verify_status": "https://lenz.io/api/v1/verify/status/{task_id}", + "verifications": "https://lenz.io/api/v1/verifications/{verification_id}", + "library": "https://lenz.io/api/v1/library", + "usage": "https://lenz.io/api/v1/me/usage" + }, + "sdks": { + "python": { + "repository": "https://github.com/lenzhq/lenz-io-python", + "package": "https://pypi.org/project/lenz-io/", + "install": "pip install lenz-io" + }, + "node": { + "repository": "https://github.com/lenzhq/lenz-io-node", + "package": "https://www.npmjs.com/package/lenz-io", + "install": "npm install lenz-io" + } + }, + "mcp": { + "url": "https://lenz.io/mcp", + "transport": "streamable-http", + "repository": "https://github.com/lenzhq/lenz-mcp" + } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__ask_history_after_reset.json b/tests/fixtures/parity/legacy/account__ask_history_after_reset.json new file mode 100644 index 0000000..e9962b2 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__ask_history_after_reset.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "body": { + "messages": [], + "exchanges_used": 0, + "exchange_limit": 100, + "can_send": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__ask_history_after_send.json b/tests/fixtures/parity/legacy/account__ask_history_after_send.json new file mode 100644 index 0000000..ded388d --- /dev/null +++ b/tests/fixtures/parity/legacy/account__ask_history_after_send.json @@ -0,0 +1,25 @@ +{ + "status": 200, + "body": { + "messages": [ + { + "role": "user", + "content": "Why?", + "created_at": "2026-10-01T09:07:00.123456+00:00" + }, + { + "role": "expert", + "content": "Because **evidence**.", + "created_at": "2026-10-01T09:14:00.123456+00:00" + } + ], + "exchanges_used": 1, + "exchange_limit": 100, + "can_send": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__ask_history_empty.json b/tests/fixtures/parity/legacy/account__ask_history_empty.json new file mode 100644 index 0000000..e9962b2 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__ask_history_empty.json @@ -0,0 +1,14 @@ +{ + "status": 200, + "body": { + "messages": [], + "exchanges_used": 0, + "exchange_limit": 100, + "can_send": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__ask_history_messages.json b/tests/fixtures/parity/legacy/account__ask_history_messages.json new file mode 100644 index 0000000..efc4809 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__ask_history_messages.json @@ -0,0 +1,25 @@ +{ + "status": 200, + "body": { + "messages": [ + { + "role": "user", + "content": "Hello", + "created_at": "2026-10-01T09:07:00.123456+00:00" + }, + { + "role": "expert", + "content": "Hi there!", + "created_at": "2026-10-01T09:14:00.123456+00:00" + } + ], + "exchanges_used": 0, + "exchange_limit": 100, + "can_send": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__ask_reset.json b/tests/fixtures/parity/legacy/account__ask_reset.json new file mode 100644 index 0000000..cddadae --- /dev/null +++ b/tests/fixtures/parity/legacy/account__ask_reset.json @@ -0,0 +1,11 @@ +{ + "status": 200, + "body": { + "ok": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__ask_send.json b/tests/fixtures/parity/legacy/account__ask_send.json new file mode 100644 index 0000000..d07ac92 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__ask_send.json @@ -0,0 +1,13 @@ +{ + "status": 200, + "body": { + "role": "expert", + "content": "Because **evidence**.", + "created_at": "2026-10-01T09:07:00.123456+00:00" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__ask_send_language.json b/tests/fixtures/parity/legacy/account__ask_send_language.json new file mode 100644 index 0000000..37c151c --- /dev/null +++ b/tests/fixtures/parity/legacy/account__ask_send_language.json @@ -0,0 +1,13 @@ +{ + "status": 200, + "body": { + "role": "expert", + "content": "Porque.", + "created_at": "2026-10-01T09:07:00.123456+00:00" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__library_empty.json b/tests/fixtures/parity/legacy/account__library_empty.json index b419316..406be8e 100644 --- a/tests/fixtures/parity/legacy/account__library_empty.json +++ b/tests/fixtures/parity/legacy/account__library_empty.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 0, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__library_item_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/account__library_item_modified_at_crosses_midnight_by_minutes.json index e59c0d7..4e168ad 100644 --- a/tests/fixtures/parity/legacy/account__library_item_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/legacy/account__library_item_modified_at_crosses_midnight_by_minutes.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -26,5 +21,10 @@ "total": 1, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__library_item_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/account__library_item_modified_at_same_day_hours_apart.json index 5fb5763..43ecdcc 100644 --- a/tests/fixtures/parity/legacy/account__library_item_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/legacy/account__library_item_modified_at_same_day_hours_apart.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -26,5 +21,10 @@ "total": 1, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__library_items.json b/tests/fixtures/parity/legacy/account__library_items.json index 2868a2e..a78c2b7 100644 --- a/tests/fixtures/parity/legacy/account__library_items.json +++ b/tests/fixtures/parity/legacy/account__library_items.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -18,12 +13,12 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "language": "en" }, { - "verification_id": "7c83247c", + "verification_id": "4d44dfaf", "claim": "Library claim 1.", "domain": "Science", "entities": [], @@ -33,12 +28,12 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "modified_at": null, "language": "en" }, { - "verification_id": "b79019e4", + "verification_id": "7c83247c", "claim": "Library claim 2.", "domain": "Science", "entities": [], @@ -48,7 +43,7 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:42:00.123456+00:00", + "created_at": "2026-10-01T09:21:00.123456+00:00", "modified_at": null, "language": "en" } @@ -56,5 +51,10 @@ "total": 3, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__library_no_match.json b/tests/fixtures/parity/legacy/account__library_no_match.json index b419316..406be8e 100644 --- a/tests/fixtures/parity/legacy/account__library_no_match.json +++ b/tests/fixtures/parity/legacy/account__library_no_match.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 0, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__library_page_past_end.json b/tests/fixtures/parity/legacy/account__library_page_past_end.json index 86d64c7..575d59b 100644 --- a/tests/fixtures/parity/legacy/account__library_page_past_end.json +++ b/tests/fixtures/parity/legacy/account__library_page_past_end.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 1, "page": 2, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__library_paginated_second_page.json b/tests/fixtures/parity/legacy/account__library_paginated_second_page.json index 9529811..67e00eb 100644 --- a/tests/fixtures/parity/legacy/account__library_paginated_second_page.json +++ b/tests/fixtures/parity/legacy/account__library_paginated_second_page.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -18,12 +13,12 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "language": "en" }, { - "verification_id": "7c83247c", + "verification_id": "4d44dfaf", "claim": "Paged claim 021.", "domain": "", "entities": [], @@ -33,7 +28,7 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "modified_at": null, "language": "en" } @@ -41,5 +36,10 @@ "total": 22, "page": 2, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__me_usage_extra_credits.json b/tests/fixtures/parity/legacy/account__me_usage_extra_credits.json index e07e31f..8eb83cf 100644 --- a/tests/fixtures/parity/legacy/account__me_usage_extra_credits.json +++ b/tests/fixtures/parity/legacy/account__me_usage_extra_credits.json @@ -1,21 +1,16 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "credits": { "total": 350, "used": 0, "remaining": 350, "extra": 250, "bonus": 250, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -62,5 +57,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__me_usage_extra_only.json b/tests/fixtures/parity/legacy/account__me_usage_extra_only.json index 0e00cac..5941290 100644 --- a/tests/fixtures/parity/legacy/account__me_usage_extra_only.json +++ b/tests/fixtures/parity/legacy/account__me_usage_extra_only.json @@ -1,21 +1,16 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "credits": { "total": 137, "used": 100, "remaining": 37, "extra": 37, "bonus": 37, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -62,5 +57,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__me_usage_extract_calls.json b/tests/fixtures/parity/legacy/account__me_usage_extract_calls.json index 335d0f1..8ed7b3c 100644 --- a/tests/fixtures/parity/legacy/account__me_usage_extract_calls.json +++ b/tests/fixtures/parity/legacy/account__me_usage_extract_calls.json @@ -1,21 +1,16 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "credits": { "total": 100, "used": 0, "remaining": 100, "extra": 0, "bonus": 0, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -62,5 +57,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__me_usage_free.json b/tests/fixtures/parity/legacy/account__me_usage_free.json index 6a23d7a..5edd565 100644 --- a/tests/fixtures/parity/legacy/account__me_usage_free.json +++ b/tests/fixtures/parity/legacy/account__me_usage_free.json @@ -1,21 +1,16 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "credits": { "total": 100, "used": 0, "remaining": 100, "extra": 0, "bonus": 0, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -62,5 +57,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__me_usage_free_partly_spent.json b/tests/fixtures/parity/legacy/account__me_usage_free_partly_spent.json index 8f43c97..7ef23a6 100644 --- a/tests/fixtures/parity/legacy/account__me_usage_free_partly_spent.json +++ b/tests/fixtures/parity/legacy/account__me_usage_free_partly_spent.json @@ -1,21 +1,16 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "credits": { "total": 100, "used": 17, "remaining": 83, "extra": 0, "bonus": 0, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -62,5 +57,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__me_usage_oauth.json b/tests/fixtures/parity/legacy/account__me_usage_oauth.json new file mode 100644 index 0000000..5edd565 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__me_usage_oauth.json @@ -0,0 +1,66 @@ +{ + "status": 200, + "body": { + "plan": "free", + "plan_label": "Free", + "quota_resets_at": "2026-10-01T14:07:00+00:00", + "credits": { + "total": 100, + "used": 0, + "remaining": 100, + "extra": 0, + "bonus": 0, + "resets_at": "2026-10-01T14:07:00+00:00" + }, + "costs": { + "verify": 10, + "assess": 1, + "ask": 1, + "citation": 1, + "extract": 0 + }, + "cost_options": { + "verify": { + "depth": { + "standard": 10, + "low": 5 + } + } + }, + "verify": { + "quota_used": 0, + "quota_total": 10, + "quota_remaining": 10, + "bonus": 0, + "credits": 0, + "remaining": 10 + }, + "ask": { + "quota_used": 0, + "quota_total": 100, + "quota_remaining": 100, + "bonus": 0, + "credits": 0, + "remaining": 100 + }, + "assess": { + "quota_used": 0, + "quota_total": 100, + "quota_remaining": 100, + "bonus": 0, + "credits": 0, + "remaining": 100 + }, + "extract": { + "calls_today": 0, + "daily_limit": 1000, + "unlimited": false + }, + "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__me_usage_plus.json b/tests/fixtures/parity/legacy/account__me_usage_plus.json index e49254d..6de7069 100644 --- a/tests/fixtures/parity/legacy/account__me_usage_plus.json +++ b/tests/fixtures/parity/legacy/account__me_usage_plus.json @@ -1,21 +1,16 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "plus", "plan_label": "Plus", - "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "quota_resets_at": "2026-10-01T14:07:00.123456+00:00", "credits": { "total": 500, "used": 0, "remaining": 500, "extra": 0, "bonus": 0, - "resets_at": "2026-10-01T09:07:00.123456+00:00" + "resets_at": "2026-10-01T14:07:00.123456+00:00" }, "costs": { "verify": 10, @@ -62,5 +57,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__me_usage_pro.json b/tests/fixtures/parity/legacy/account__me_usage_pro.json index 4c0fab9..8f14251 100644 --- a/tests/fixtures/parity/legacy/account__me_usage_pro.json +++ b/tests/fixtures/parity/legacy/account__me_usage_pro.json @@ -1,21 +1,16 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "pro", "plan_label": "Pro", - "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "quota_resets_at": "2026-10-01T14:07:00.123456+00:00", "credits": { "total": 5000, "used": 0, "remaining": 5000, "extra": 0, "bonus": 0, - "resets_at": "2026-10-01T09:07:00.123456+00:00" + "resets_at": "2026-10-01T14:07:00.123456+00:00" }, "costs": { "verify": 10, @@ -62,5 +57,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__me_usage_pro_extra.json b/tests/fixtures/parity/legacy/account__me_usage_pro_extra.json index a15d613..fe0a019 100644 --- a/tests/fixtures/parity/legacy/account__me_usage_pro_extra.json +++ b/tests/fixtures/parity/legacy/account__me_usage_pro_extra.json @@ -1,21 +1,16 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "pro", "plan_label": "Pro", - "quota_resets_at": "2026-10-01T09:07:00.123456+00:00", + "quota_resets_at": "2026-10-01T14:07:00.123456+00:00", "credits": { "total": 6000, "used": 0, "remaining": 6000, "extra": 1000, "bonus": 1000, - "resets_at": "2026-10-01T09:07:00.123456+00:00" + "resets_at": "2026-10-01T14:07:00.123456+00:00" }, "costs": { "verify": 10, @@ -62,5 +57,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__me_usage_stale_quota_row.json b/tests/fixtures/parity/legacy/account__me_usage_stale_quota_row.json index 6a23d7a..5edd565 100644 --- a/tests/fixtures/parity/legacy/account__me_usage_stale_quota_row.json +++ b/tests/fixtures/parity/legacy/account__me_usage_stale_quota_row.json @@ -1,21 +1,16 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "plan": "free", "plan_label": "Free", - "quota_resets_at": "2026-10-01T09:07:00+00:00", + "quota_resets_at": "2026-10-01T14:07:00+00:00", "credits": { "total": 100, "used": 0, "remaining": 100, "extra": 0, "bonus": 0, - "resets_at": "2026-10-01T09:07:00+00:00" + "resets_at": "2026-10-01T14:07:00+00:00" }, "costs": { "verify": 10, @@ -62,5 +57,10 @@ "unlimited": false }, "has_webhook_secret": false + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/account__oauth_register_success.json b/tests/fixtures/parity/legacy/account__oauth_register_success.json new file mode 100644 index 0000000..15e9752 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__oauth_register_success.json @@ -0,0 +1,25 @@ +{ + "status": 201, + "body": { + "client_id": "lzc_0a1db81dbba84d2d473826d8", + "client_id_issued_at": "", + "client_name": "n8n", + "redirect_uris": [ + "https://acme.app.n8n.cloud/rest/oauth2-credential/callback" + ], + "grant_types": [ + "authorization_code", + "refresh_token" + ], + "response_types": [ + "code" + ], + "token_endpoint_auth_method": "none", + "scope": "assess verify ask ask:read extract history:read history:write usage:read webhooks:manage" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__oauth_revoke.json b/tests/fixtures/parity/legacy/account__oauth_revoke.json new file mode 100644 index 0000000..f0ea1e6 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__oauth_revoke.json @@ -0,0 +1,9 @@ +{ + "status": 200, + "body": {}, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__oauth_revoke_token.json b/tests/fixtures/parity/legacy/account__oauth_revoke_token.json new file mode 100644 index 0000000..f0ea1e6 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__oauth_revoke_token.json @@ -0,0 +1,9 @@ +{ + "status": 200, + "body": {}, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__webhook_secret_api_key.json b/tests/fixtures/parity/legacy/account__webhook_secret_api_key.json new file mode 100644 index 0000000..4fdb761 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__webhook_secret_api_key.json @@ -0,0 +1,12 @@ +{ + "status": 403, + "body": { + "detail": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "code": "oauth_only" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__webhook_secret_insufficient_scope.json b/tests/fixtures/parity/legacy/account__webhook_secret_insufficient_scope.json new file mode 100644 index 0000000..6e2d697 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__webhook_secret_insufficient_scope.json @@ -0,0 +1,14 @@ +{ + "status": 403, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "webhooks:manage" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"webhooks:manage\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__webhook_secret_oauth.json b/tests/fixtures/parity/legacy/account__webhook_secret_oauth.json new file mode 100644 index 0000000..65ec897 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__webhook_secret_oauth.json @@ -0,0 +1,11 @@ +{ + "status": 200, + "body": { + "webhook_secret": "dc06a4fb5512d77eb96f64eedf3ce77dff58de30e17140fbb229a3e048c3bb93" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/assess__401_no_key.json b/tests/fixtures/parity/legacy/assess__401_no_key.json index 8dd2fb4..54ec0b6 100644 --- a/tests/fixtures/parity/legacy/assess__401_no_key.json +++ b/tests/fixtures/parity/legacy/assess__401_no_key.json @@ -1,11 +1,11 @@ { "status": 401, + "body": { + "detail": "Unauthorized" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized" } } diff --git a/tests/fixtures/parity/legacy/assess__402_no_credits.json b/tests/fixtures/parity/legacy/assess__402_no_credits.json index 1cd3038..50f67b4 100644 --- a/tests/fixtures/parity/legacy/assess__402_no_credits.json +++ b/tests/fixtures/parity/legacy/assess__402_no_credits.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining /assess units.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__422_blank_item.json b/tests/fixtures/parity/legacy/assess__422_blank_item.json index af5a22a..94cac74 100644 --- a/tests/fixtures/parity/legacy/assess__422_blank_item.json +++ b/tests/fixtures/parity/legacy/assess__422_blank_item.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "claims[1] is blank.", + "code": "blank_item" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "claims[1] is blank.", - "code": "blank_item" } } diff --git a/tests/fixtures/parity/legacy/assess__422_blank_text.json b/tests/fixtures/parity/legacy/assess__422_blank_text.json index 1ebb88e..367f620 100644 --- a/tests/fixtures/parity/legacy/assess__422_blank_text.json +++ b/tests/fixtures/parity/legacy/assess__422_blank_text.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Text is required." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Text is required." } } diff --git a/tests/fixtures/parity/legacy/assess__422_input_conflict.json b/tests/fixtures/parity/legacy/assess__422_input_conflict.json index c811d89..5f709c8 100644 --- a/tests/fixtures/parity/legacy/assess__422_input_conflict.json +++ b/tests/fixtures/parity/legacy/assess__422_input_conflict.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "Send either `claim` (one text) or `claims` (a list of claims), not both.", + "code": "input_conflict" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Send either `claim` (one text) or `claims` (a list of claims), not both.", - "code": "input_conflict" } } diff --git a/tests/fixtures/parity/legacy/assess__422_item_too_long.json b/tests/fixtures/parity/legacy/assess__422_item_too_long.json index 21e6f19..34cbc4d 100644 --- a/tests/fixtures/parity/legacy/assess__422_item_too_long.json +++ b/tests/fixtures/parity/legacy/assess__422_item_too_long.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.", + "code": "item_too_long" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "claims[1] is 2001 characters; each claim may be at most 2000. Send a document to /extract first, then assess its claims.", - "code": "item_too_long" } } diff --git a/tests/fixtures/parity/legacy/assess__422_no_input_field.json b/tests/fixtures/parity/legacy/assess__422_no_input_field.json index f862968..591c22f 100644 --- a/tests/fixtures/parity/legacy/assess__422_no_input_field.json +++ b/tests/fixtures/parity/legacy/assess__422_no_input_field.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -17,5 +12,10 @@ "msg": "Field required" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__422_too_many_items.json b/tests/fixtures/parity/legacy/assess__422_too_many_items.json index 80d556a..4c88204 100644 --- a/tests/fixtures/parity/legacy/assess__422_too_many_items.json +++ b/tests/fixtures/parity/legacy/assess__422_too_many_items.json @@ -1,12 +1,12 @@ { "status": 422, + "body": { + "detail": "At most 20 claims per call; got 21. Split the list.", + "code": "too_many_items" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "At most 20 claims per call; got 21. Split the list.", - "code": "too_many_items" } } diff --git a/tests/fixtures/parity/legacy/assess__422_wrong_type_claim.json b/tests/fixtures/parity/legacy/assess__422_wrong_type_claim.json index d8d8c87..dd070e7 100644 --- a/tests/fixtures/parity/legacy/assess__422_wrong_type_claim.json +++ b/tests/fixtures/parity/legacy/assess__422_wrong_type_claim.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -17,5 +12,10 @@ "msg": "Input should be a valid string" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__422_wrong_type_claims.json b/tests/fixtures/parity/legacy/assess__422_wrong_type_claims.json index a997f8c..0a135da 100644 --- a/tests/fixtures/parity/legacy/assess__422_wrong_type_claims.json +++ b/tests/fixtures/parity/legacy/assess__422_wrong_type_claims.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -17,5 +12,10 @@ "msg": "Input should be a valid list" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__502_framing_failed.json b/tests/fixtures/parity/legacy/assess__502_framing_failed.json index 9b6d047..a15f201 100644 --- a/tests/fixtures/parity/legacy/assess__502_framing_failed.json +++ b/tests/fixtures/parity/legacy/assess__502_framing_failed.json @@ -1,13 +1,13 @@ { "status": 502, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Framing failed", "code": "framing_failed", "error": "framing_failed" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__503_capacity.json b/tests/fixtures/parity/legacy/assess__503_capacity.json index de01494..4c5b2c2 100644 --- a/tests/fixtures/parity/legacy/assess__503_capacity.json +++ b/tests/fixtures/parity/legacy/assess__503_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "100", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 100, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "100", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__503_list_all_upstream_unavailable.json b/tests/fixtures/parity/legacy/assess__503_list_all_upstream_unavailable.json index 9e74dd2..d37b434 100644 --- a/tests/fixtures/parity/legacy/assess__503_list_all_upstream_unavailable.json +++ b/tests/fixtures/parity/legacy/assess__503_list_all_upstream_unavailable.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Our model providers are temporarily unavailable. Please retry shortly. Nothing was charged.", "code": "upstream_unavailable", "retry_after": 90, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__503_upstream_unavailable.json b/tests/fixtures/parity/legacy/assess__503_upstream_unavailable.json index b308e0c..9308b85 100644 --- a/tests/fixtures/parity/legacy/assess__503_upstream_unavailable.json +++ b/tests/fixtures/parity/legacy/assess__503_upstream_unavailable.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Our model providers are temporarily unavailable. Please retry shortly.", "code": "upstream_unavailable", "retry_after": 90, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__deep_tier_hit.json b/tests/fixtures/parity/legacy/assess__deep_tier_hit.json index a5e696a..ec51c5f 100644 --- a/tests/fixtures/parity/legacy/assess__deep_tier_hit.json +++ b/tests/fixtures/parity/legacy/assess__deep_tier_hit.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -24,5 +19,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__idempotency_body_mismatch.json b/tests/fixtures/parity/legacy/assess__idempotency_body_mismatch.json index ef04562..2425649 100644 --- a/tests/fixtures/parity/legacy/assess__idempotency_body_mismatch.json +++ b/tests/fixtures/parity/legacy/assess__idempotency_body_mismatch.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body." } } diff --git a/tests/fixtures/parity/legacy/assess__idempotency_first.json b/tests/fixtures/parity/legacy/assess__idempotency_first.json index bac5c15..fedc9c5 100644 --- a/tests/fixtures/parity/legacy/assess__idempotency_first.json +++ b/tests/fixtures/parity/legacy/assess__idempotency_first.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -24,5 +19,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__idempotency_replay.json b/tests/fixtures/parity/legacy/assess__idempotency_replay.json index bac5c15..fedc9c5 100644 --- a/tests/fixtures/parity/legacy/assess__idempotency_replay.json +++ b/tests/fixtures/parity/legacy/assess__idempotency_replay.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -24,5 +19,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__list_all_error_rows.json b/tests/fixtures/parity/legacy/assess__list_all_error_rows.json index ba846f5..d376f1f 100644 --- a/tests/fixtures/parity/legacy/assess__list_all_error_rows.json +++ b/tests/fixtures/parity/legacy/assess__list_all_error_rows.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -38,5 +33,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__list_compound_item.json b/tests/fixtures/parity/legacy/assess__list_compound_item.json index 2a9d928..cc02da1 100644 --- a/tests/fixtures/parity/legacy/assess__list_compound_item.json +++ b/tests/fixtures/parity/legacy/assess__list_compound_item.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -41,5 +36,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__list_mixed_rows.json b/tests/fixtures/parity/legacy/assess__list_mixed_rows.json index 5c4bfe1..2143d2d 100644 --- a/tests/fixtures/parity/legacy/assess__list_mixed_rows.json +++ b/tests/fixtures/parity/legacy/assess__list_mixed_rows.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -66,5 +61,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__memo_hit.json b/tests/fixtures/parity/legacy/assess__memo_hit.json index 96a0c4d..895b4d1 100644 --- a/tests/fixtures/parity/legacy/assess__memo_hit.json +++ b/tests/fixtures/parity/legacy/assess__memo_hit.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -24,5 +19,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__replay_later_200.json b/tests/fixtures/parity/legacy/assess__replay_later_200.json index bac5c15..fedc9c5 100644 --- a/tests/fixtures/parity/legacy/assess__replay_later_200.json +++ b/tests/fixtures/parity/legacy/assess__replay_later_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -24,5 +19,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__single_no_claim.json b/tests/fixtures/parity/legacy/assess__single_no_claim.json index b40abf7..30e6d5f 100644 --- a/tests/fixtures/parity/legacy/assess__single_no_claim.json +++ b/tests/fixtures/parity/legacy/assess__single_no_claim.json @@ -1,15 +1,15 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [], "error": "No verifiable claim detected", "error_code": "no_claim", "candidate_claims": [], "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__single_one_claim.json b/tests/fixtures/parity/legacy/assess__single_one_claim.json index d27e7eb..329438b 100644 --- a/tests/fixtures/parity/legacy/assess__single_one_claim.json +++ b/tests/fixtures/parity/legacy/assess__single_one_claim.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -24,5 +19,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__single_text_over_wave_more_claims.json b/tests/fixtures/parity/legacy/assess__single_text_over_wave_more_claims.json index d4849fc..fdc4751 100644 --- a/tests/fixtures/parity/legacy/assess__single_text_over_wave_more_claims.json +++ b/tests/fixtures/parity/legacy/assess__single_text_over_wave_more_claims.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -293,5 +288,10 @@ "Claim number 20 is documented.", "Claim number 21 is documented." ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__single_text_several_claims.json b/tests/fixtures/parity/legacy/assess__single_text_several_claims.json index 2db91ed..ffce99c 100644 --- a/tests/fixtures/parity/legacy/assess__single_text_several_claims.json +++ b/tests/fixtures/parity/legacy/assess__single_text_several_claims.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -52,5 +47,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/assess__suggest_rewrite.json b/tests/fixtures/parity/legacy/assess__suggest_rewrite.json index f4f7b54..68f4fa2 100644 --- a/tests/fixtures/parity/legacy/assess__suggest_rewrite.json +++ b/tests/fixtures/parity/legacy/assess__suggest_rewrite.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "claims": [ { @@ -38,5 +33,10 @@ ], "error": null, "more_claims": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__402_no_credits.json b/tests/fixtures/parity/legacy/citecheck__402_no_credits.json index 200f42b..a73ca33 100644 --- a/tests/fixtures/parity/legacy/citecheck__402_no_credits.json +++ b/tests/fixtures/parity/legacy/citecheck__402_no_credits.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining credits for citation checks.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 100, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 100, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_empty_body.json b/tests/fixtures/parity/legacy/citecheck__422_empty_body.json index 6f9d742..8fde0be 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_empty_body.json +++ b/tests/fixtures/parity/legacy/citecheck__422_empty_body.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload: Value error, send exactly one of text and pairs", "code": "validation_error", @@ -17,5 +12,10 @@ "msg": "Value error, send exactly one of text and pairs" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_invalid_doi.json b/tests/fixtures/parity/legacy/citecheck__422_invalid_doi.json index 3026138..7177677 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_invalid_doi.json +++ b/tests/fixtures/parity/legacy/citecheck__422_invalid_doi.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.pairs.0.doi: Send the DOI alone, like 10.1038/nature12373.", "code": "invalid_doi", @@ -20,5 +15,10 @@ "msg": "Send the DOI alone, like 10.1038/nature12373." } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_max_citations_over_20.json b/tests/fixtures/parity/legacy/citecheck__422_max_citations_over_20.json index 279eba0..710466e 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_max_citations_over_20.json +++ b/tests/fixtures/parity/legacy/citecheck__422_max_citations_over_20.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.max_citations: Input should be less than or equal to 20", "code": "validation_error", @@ -18,5 +13,10 @@ "msg": "Input should be less than or equal to 20" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_max_citations_with_pairs.json b/tests/fixtures/parity/legacy/citecheck__422_max_citations_with_pairs.json index bd0828b..218964a 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_max_citations_with_pairs.json +++ b/tests/fixtures/parity/legacy/citecheck__422_max_citations_with_pairs.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload: Value error, max_citations goes with text: every pair is checked", "code": "validation_error", @@ -17,5 +12,10 @@ "msg": "Value error, max_citations goes with text: every pair is checked" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_pair_bad_url_scheme.json b/tests/fixtures/parity/legacy/citecheck__422_pair_bad_url_scheme.json index c7046cc..b557fbf 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_pair_bad_url_scheme.json +++ b/tests/fixtures/parity/legacy/citecheck__422_pair_bad_url_scheme.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.pairs.0.url: Value error, url: an http or https link", "code": "validation_error", @@ -20,5 +15,10 @@ "msg": "Value error, url: an http or https link" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_pair_no_source.json b/tests/fixtures/parity/legacy/citecheck__422_pair_no_source.json index 10ddcdb..0619ab6 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_pair_no_source.json +++ b/tests/fixtures/parity/legacy/citecheck__422_pair_no_source.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.pairs.0: Value error, send exactly one of url and doi", "code": "validation_error", @@ -19,5 +14,10 @@ "msg": "Value error, send exactly one of url and doi" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_pair_url_and_doi.json b/tests/fixtures/parity/legacy/citecheck__422_pair_url_and_doi.json index 10ddcdb..0619ab6 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_pair_url_and_doi.json +++ b/tests/fixtures/parity/legacy/citecheck__422_pair_url_and_doi.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.pairs.0: Value error, send exactly one of url and doi", "code": "validation_error", @@ -19,5 +14,10 @@ "msg": "Value error, send exactly one of url and doi" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_pairs_empty.json b/tests/fixtures/parity/legacy/citecheck__422_pairs_empty.json index 2df0724..edc2537 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_pairs_empty.json +++ b/tests/fixtures/parity/legacy/citecheck__422_pairs_empty.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.pairs: Value error, pairs: 1 to 20 items", "code": "validation_error", @@ -18,5 +13,10 @@ "msg": "Value error, pairs: 1 to 20 items" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_pairs_over_20.json b/tests/fixtures/parity/legacy/citecheck__422_pairs_over_20.json index 2df0724..edc2537 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_pairs_over_20.json +++ b/tests/fixtures/parity/legacy/citecheck__422_pairs_over_20.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.pairs: Value error, pairs: 1 to 20 items", "code": "validation_error", @@ -18,5 +13,10 @@ "msg": "Value error, pairs: 1 to 20 items" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_quote_not_in_statement.json b/tests/fixtures/parity/legacy/citecheck__422_quote_not_in_statement.json index 462190c..9c4f0a3 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_quote_not_in_statement.json +++ b/tests/fixtures/parity/legacy/citecheck__422_quote_not_in_statement.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.pairs.0: Each quote must be words of its statement.", "code": "quote_not_in_statement", @@ -19,5 +14,10 @@ "msg": "Each quote must be words of its statement." } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_text_and_pairs.json b/tests/fixtures/parity/legacy/citecheck__422_text_and_pairs.json index 6f9d742..8fde0be 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_text_and_pairs.json +++ b/tests/fixtures/parity/legacy/citecheck__422_text_and_pairs.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload: Value error, send exactly one of text and pairs", "code": "validation_error", @@ -17,5 +12,10 @@ "msg": "Value error, send exactly one of text and pairs" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_unknown_field.json b/tests/fixtures/parity/legacy/citecheck__422_unknown_field.json index cb50c65..b63a6d8 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_unknown_field.json +++ b/tests/fixtures/parity/legacy/citecheck__422_unknown_field.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.pairs.0.extra: Extra inputs are not permitted", "code": "validation_error", @@ -20,5 +15,10 @@ "msg": "Extra inputs are not permitted" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_url_input.json b/tests/fixtures/parity/legacy/citecheck__422_url_input.json index d5ddd25..c6786a8 100644 --- a/tests/fixtures/parity/legacy/citecheck__422_url_input.json +++ b/tests/fixtures/parity/legacy/citecheck__422_url_input.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Send the page's text, with its links, to check the sources it cites.", "code": "url_input", @@ -17,5 +12,10 @@ "msg": "Send the page's text, with its links, to check the sources it cites." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__422_webhook_secret_missing.json b/tests/fixtures/parity/legacy/citecheck__422_webhook_secret_missing.json new file mode 100644 index 0000000..1ecf2d1 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__422_webhook_secret_missing.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "body": { + "detail": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "code": "webhook_secret_missing", + "errors": [ + { + "loc": [ + "body", + "webhook_url" + ], + "msg": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend." + } + ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__429_citecheck_in_flight.json b/tests/fixtures/parity/legacy/citecheck__429_citecheck_in_flight.json index 5222210..5ae869f 100644 --- a/tests/fixtures/parity/legacy/citecheck__429_citecheck_in_flight.json +++ b/tests/fixtures/parity/legacy/citecheck__429_citecheck_in_flight.json @@ -1,14 +1,14 @@ { "status": 429, + "body": { + "detail": "This account already has 3 citation checks running. Wait for one to finish.", + "code": "citecheck_in_flight", + "retry_after_seconds": 60 + }, "headers": { "Content-Type": "application/json", "Retry-After": "60", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "This account already has 3 citation checks running. Wait for one to finish.", - "code": "citecheck_in_flight", - "retry_after_seconds": 60 } } diff --git a/tests/fixtures/parity/legacy/citecheck__503_capacity.json b/tests/fixtures/parity/legacy/citecheck__503_capacity.json index adcc209..085990c 100644 --- a/tests/fixtures/parity/legacy/citecheck__503_capacity.json +++ b/tests/fixtures/parity/legacy/citecheck__503_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 90, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__503_citations_unavailable.json b/tests/fixtures/parity/legacy/citecheck__503_citations_unavailable.json index ea4e7b8..3be14e4 100644 --- a/tests/fixtures/parity/legacy/citecheck__503_citations_unavailable.json +++ b/tests/fixtures/parity/legacy/citecheck__503_citations_unavailable.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "300", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Citation checking is switched off right now. Retry later; nothing was charged.", "code": "citations_unavailable", "retry_after": 300, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "300", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_404_not_found.json b/tests/fixtures/parity/legacy/citecheck__get_404_not_found.json index e0adfb3..5aa3709 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_404_not_found.json +++ b/tests/fixtures/parity/legacy/citecheck__get_404_not_found.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Citation check not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Citation check not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_404_review_id.json b/tests/fixtures/parity/legacy/citecheck__get_404_review_id.json index e0adfb3..5aa3709 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_404_review_id.json +++ b/tests/fixtures/parity/legacy/citecheck__get_404_review_id.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Citation check not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Citation check not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_410_purged.json b/tests/fixtures/parity/legacy/citecheck__get_410_purged.json index 9fff981..08b9585 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_410_purged.json +++ b/tests/fixtures/parity/legacy/citecheck__get_410_purged.json @@ -1,13 +1,13 @@ { "status": 410, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This check is no longer available: its account removes content after a set period.", "code": "purged", "purged_at": "2026-10-01T09:07:00.123456Z" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_checking.json b/tests/fixtures/parity/legacy/citecheck__get_checking.json index 3c32986..57a5d80 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_checking.json +++ b/tests/fixtures/parity/legacy/citecheck__get_checking.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "checking", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "poll_after_seconds": 10, @@ -99,5 +94,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_clean.json b/tests/fixtures/parity/legacy/citecheck__get_completed_clean.json index 5e92112..2cb9173 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_completed_clean.json +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_clean.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -107,5 +102,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_doi_pair.json b/tests/fixtures/parity/legacy/citecheck__get_completed_doi_pair.json index 2bdb1bf..bc7c684 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_completed_doi_pair.json +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_doi_pair.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -72,5 +67,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_incomplete.json b/tests/fixtures/parity/legacy/citecheck__get_completed_incomplete.json index 316b4d5..beddca7 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_completed_incomplete.json +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_incomplete.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -123,5 +118,10 @@ ], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_issues_found.json b/tests/fixtures/parity/legacy/citecheck__get_completed_issues_found.json index 75b7f7d..e730c7e 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_completed_issues_found.json +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_issues_found.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -137,5 +132,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_partly_supported.json b/tests/fixtures/parity/legacy/citecheck__get_completed_partly_supported.json index 5ae2366..8f1a891 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_completed_partly_supported.json +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_partly_supported.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -107,5 +102,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_completed_unchecked.json b/tests/fixtures/parity/legacy/citecheck__get_completed_unchecked.json index 4e53bd4..bc4922d 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_completed_unchecked.json +++ b/tests/fixtures/parity/legacy/citecheck__get_completed_unchecked.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -107,5 +102,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_failed_no_citations.json b/tests/fixtures/parity/legacy/citecheck__get_failed_no_citations.json index c66a84e..6a9084b 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_failed_no_citations.json +++ b/tests/fixtures/parity/legacy/citecheck__get_failed_no_citations.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -38,5 +33,10 @@ "docs_url": "https://lenz.io/docs/errors#invalid-input" }, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_failed_upstream_unavailable.json b/tests/fixtures/parity/legacy/citecheck__get_failed_upstream_unavailable.json index 2fba44d..9f29da1 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_failed_upstream_unavailable.json +++ b/tests/fixtures/parity/legacy/citecheck__get_failed_upstream_unavailable.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -144,5 +139,10 @@ "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" }, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_queued.json b/tests/fixtures/parity/legacy/citecheck__get_queued.json index 3e7d4d1..85fbb28 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_queued.json +++ b/tests/fixtures/parity/legacy/citecheck__get_queued.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "queued", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "poll_after_seconds": 10, @@ -68,5 +63,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_text_limit_exactly_at_limit.json b/tests/fixtures/parity/legacy/citecheck__get_text_limit_exactly_at_limit.json index 0ff4ff1..8740719 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_text_limit_exactly_at_limit.json +++ b/tests/fixtures/parity/legacy/citecheck__get_text_limit_exactly_at_limit.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -115,5 +110,10 @@ "citation_failures": [], "failure": null, "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__get_text_limit_reached.json b/tests/fixtures/parity/legacy/citecheck__get_text_limit_reached.json index 764e8b1..c432a1c 100644 --- a/tests/fixtures/parity/legacy/citecheck__get_text_limit_reached.json +++ b/tests/fixtures/parity/legacy/citecheck__get_text_limit_reached.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "citecheck_id": "8eb49302", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -89,5 +84,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__idempotency_body_mismatch_422.json b/tests/fixtures/parity/legacy/citecheck__idempotency_body_mismatch_422.json index fc47586..1c526f0 100644 --- a/tests/fixtures/parity/legacy/citecheck__idempotency_body_mismatch_422.json +++ b/tests/fixtures/parity/legacy/citecheck__idempotency_body_mismatch_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Idempotency-Key reused with a different request body.", "code": "idempotency_body_mismatch", @@ -16,5 +11,10 @@ "msg": "Idempotency-Key reused with a different request body." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__idempotency_conflict_409.json b/tests/fixtures/parity/legacy/citecheck__idempotency_conflict_409.json index 3978a83..c29d230 100644 --- a/tests/fixtures/parity/legacy/citecheck__idempotency_conflict_409.json +++ b/tests/fixtures/parity/legacy/citecheck__idempotency_conflict_409.json @@ -1,13 +1,13 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "A check with this Idempotency-Key is still being created. Retry shortly.", "code": "idempotency_conflict", "citecheck_id": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/citecheck__idempotent_replay_202.json b/tests/fixtures/parity/legacy/citecheck__idempotent_replay_202.json index 864551e..43d3598 100644 --- a/tests/fixtures/parity/legacy/citecheck__idempotent_replay_202.json +++ b/tests/fixtures/parity/legacy/citecheck__idempotent_replay_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/citechecks/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "citecheck_id": "8eb49302", - "status": "queued" } } diff --git a/tests/fixtures/parity/legacy/citecheck__receipt_202.json b/tests/fixtures/parity/legacy/citecheck__receipt_202.json index 864551e..43d3598 100644 --- a/tests/fixtures/parity/legacy/citecheck__receipt_202.json +++ b/tests/fixtures/parity/legacy/citecheck__receipt_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/citechecks/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "citecheck_id": "8eb49302", - "status": "queued" } } diff --git a/tests/fixtures/parity/legacy/citecheck__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/legacy/citecheck__receipt_202_empty_webhook_url.json index 864551e..43d3598 100644 --- a/tests/fixtures/parity/legacy/citecheck__receipt_202_empty_webhook_url.json +++ b/tests/fixtures/parity/legacy/citecheck__receipt_202_empty_webhook_url.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "citecheck_id": "8eb49302", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/citechecks/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "citecheck_id": "8eb49302", - "status": "queued" } } diff --git a/tests/fixtures/parity/legacy/errors__ask_bad_language.json b/tests/fixtures/parity/legacy/errors__ask_bad_language.json index 2d6c4b1..ea416f6 100644 --- a/tests/fixtures/parity/legacy/errors__ask_bad_language.json +++ b/tests/fixtures/parity/legacy/errors__ask_bad_language.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unsupported language 'xx-nope'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact." } } diff --git a/tests/fixtures/parity/legacy/errors__ask_empty_message.json b/tests/fixtures/parity/legacy/errors__ask_empty_message.json index 1eee180..5ee6ef6 100644 --- a/tests/fixtures/parity/legacy/errors__ask_empty_message.json +++ b/tests/fixtures/parity/legacy/errors__ask_empty_message.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Message cannot be empty." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Message cannot be empty." } } diff --git a/tests/fixtures/parity/legacy/errors__ask_failed.json b/tests/fixtures/parity/legacy/errors__ask_failed.json index 6eb6b31..f3c8931 100644 --- a/tests/fixtures/parity/legacy/errors__ask_failed.json +++ b/tests/fixtures/parity/legacy/errors__ask_failed.json @@ -1,13 +1,13 @@ { "status": 502, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "The chat model could not answer this question.", "code": "ask_failed", "error": "ask_failed" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__ask_idempotency_key_reused.json b/tests/fixtures/parity/legacy/errors__ask_idempotency_key_reused.json index ef04562..2425649 100644 --- a/tests/fixtures/parity/legacy/errors__ask_idempotency_key_reused.json +++ b/tests/fixtures/parity/legacy/errors__ask_idempotency_key_reused.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body." } } diff --git a/tests/fixtures/parity/legacy/errors__ask_message_too_long.json b/tests/fixtures/parity/legacy/errors__ask_message_too_long.json index 659caed..3ad27a5 100644 --- a/tests/fixtures/parity/legacy/errors__ask_message_too_long.json +++ b/tests/fixtures/parity/legacy/errors__ask_message_too_long.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Message too long (max 500 characters)." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Message too long (max 500 characters)." } } diff --git a/tests/fixtures/parity/legacy/errors__ask_not_completed.json b/tests/fixtures/parity/legacy/errors__ask_not_completed.json index b59d747..6f7da62 100644 --- a/tests/fixtures/parity/legacy/errors__ask_not_completed.json +++ b/tests/fixtures/parity/legacy/errors__ask_not_completed.json @@ -1,11 +1,11 @@ { "status": 400, + "body": { + "detail": "Ask is only available for completed verifications." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Ask is only available for completed verifications." } } diff --git a/tests/fixtures/parity/legacy/errors__ask_reset_not_found.json b/tests/fixtures/parity/legacy/errors__ask_reset_not_found.json index c0d18e1..65ff442 100644 --- a/tests/fixtures/parity/legacy/errors__ask_reset_not_found.json +++ b/tests/fixtures/parity/legacy/errors__ask_reset_not_found.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Verification not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Verification not found." } } diff --git a/tests/fixtures/parity/legacy/errors__ask_send_not_found.json b/tests/fixtures/parity/legacy/errors__ask_send_not_found.json index c0d18e1..65ff442 100644 --- a/tests/fixtures/parity/legacy/errors__ask_send_not_found.json +++ b/tests/fixtures/parity/legacy/errors__ask_send_not_found.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Verification not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Verification not found." } } diff --git a/tests/fixtures/parity/legacy/errors__auth_bad_key.json b/tests/fixtures/parity/legacy/errors__auth_bad_key.json index 8dd2fb4..54ec0b6 100644 --- a/tests/fixtures/parity/legacy/errors__auth_bad_key.json +++ b/tests/fixtures/parity/legacy/errors__auth_bad_key.json @@ -1,11 +1,11 @@ { "status": 401, + "body": { + "detail": "Unauthorized" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized" } } diff --git a/tests/fixtures/parity/legacy/errors__auth_bad_oauth_token.json b/tests/fixtures/parity/legacy/errors__auth_bad_oauth_token.json new file mode 100644 index 0000000..cc8ef29 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_bad_oauth_token.json @@ -0,0 +1,13 @@ +{ + "status": 401, + "body": { + "detail": "The access token is invalid, expired or revoked.", + "code": "invalid_token" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"invalid_token\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_basic_scheme.json b/tests/fixtures/parity/legacy/errors__auth_basic_scheme.json index 8dd2fb4..54ec0b6 100644 --- a/tests/fixtures/parity/legacy/errors__auth_basic_scheme.json +++ b/tests/fixtures/parity/legacy/errors__auth_basic_scheme.json @@ -1,11 +1,11 @@ { "status": 401, + "body": { + "detail": "Unauthorized" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized" } } diff --git a/tests/fixtures/parity/legacy/errors__auth_expired_oauth_token.json b/tests/fixtures/parity/legacy/errors__auth_expired_oauth_token.json new file mode 100644 index 0000000..cc8ef29 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_expired_oauth_token.json @@ -0,0 +1,13 @@ +{ + "status": 401, + "body": { + "detail": "The access token is invalid, expired or revoked.", + "code": "invalid_token" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "WWW-Authenticate": "Bearer error=\"invalid_token\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_insufficient_scope.json b/tests/fixtures/parity/legacy/errors__auth_insufficient_scope.json index 15b8696..4c2fcdd 100644 --- a/tests/fixtures/parity/legacy/errors__auth_insufficient_scope.json +++ b/tests/fixtures/parity/legacy/errors__auth_insufficient_scope.json @@ -1,14 +1,14 @@ { "status": 403, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "verify" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"verify\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "The access token does not allow this call.", - "code": "insufficient_scope", - "scope": "verify" } } diff --git a/tests/fixtures/parity/legacy/errors__auth_missing_bearer.json b/tests/fixtures/parity/legacy/errors__auth_missing_bearer.json index 8dd2fb4..54ec0b6 100644 --- a/tests/fixtures/parity/legacy/errors__auth_missing_bearer.json +++ b/tests/fixtures/parity/legacy/errors__auth_missing_bearer.json @@ -1,11 +1,11 @@ { "status": 401, + "body": { + "detail": "Unauthorized" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized" } } diff --git a/tests/fixtures/parity/legacy/errors__auth_missing_bearer_post.json b/tests/fixtures/parity/legacy/errors__auth_missing_bearer_post.json index 8dd2fb4..54ec0b6 100644 --- a/tests/fixtures/parity/legacy/errors__auth_missing_bearer_post.json +++ b/tests/fixtures/parity/legacy/errors__auth_missing_bearer_post.json @@ -1,11 +1,11 @@ { "status": 401, + "body": { + "detail": "Unauthorized" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized" } } diff --git a/tests/fixtures/parity/legacy/errors__auth_oauth_issuer_off.json b/tests/fixtures/parity/legacy/errors__auth_oauth_issuer_off.json new file mode 100644 index 0000000..54ec0b6 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_oauth_issuer_off.json @@ -0,0 +1,11 @@ +{ + "status": 401, + "body": { + "detail": "Unauthorized" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_oauth_only.json b/tests/fixtures/parity/legacy/errors__auth_oauth_only.json new file mode 100644 index 0000000..4fdb761 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__auth_oauth_only.json @@ -0,0 +1,12 @@ +{ + "status": 403, + "body": { + "detail": "An API key's webhook secret is managed on the API credentials page (https://lenz.io/api-credentials). This endpoint is for OAuth connections.", + "code": "oauth_only" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_history.json b/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_history.json index e691e72..627261e 100644 --- a/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_history.json +++ b/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_history.json @@ -1,13 +1,13 @@ { "status": 403, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This report is private.", "code": "private_claim", "error": "private_claim" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_send.json b/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_send.json index e691e72..627261e 100644 --- a/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_send.json +++ b/tests/fixtures/parity/legacy/errors__auth_private_claim_ask_send.json @@ -1,13 +1,13 @@ { "status": 403, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This report is private.", "code": "private_claim", "error": "private_claim" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__auth_unrecognised_prefix.json b/tests/fixtures/parity/legacy/errors__auth_unrecognised_prefix.json index 8dd2fb4..54ec0b6 100644 --- a/tests/fixtures/parity/legacy/errors__auth_unrecognised_prefix.json +++ b/tests/fixtures/parity/legacy/errors__auth_unrecognised_prefix.json @@ -1,11 +1,11 @@ { "status": 401, + "body": { + "detail": "Unauthorized" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized" } } diff --git a/tests/fixtures/parity/legacy/errors__http_error_library_bad_curated.json b/tests/fixtures/parity/legacy/errors__http_error_library_bad_curated.json index 03f1508..0a644ad 100644 --- a/tests/fixtures/parity/legacy/errors__http_error_library_bad_curated.json +++ b/tests/fixtures/parity/legacy/errors__http_error_library_bad_curated.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Invalid curated list(s). Allowed: trivia" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Invalid curated list(s). Allowed: trivia" } } diff --git a/tests/fixtures/parity/legacy/errors__http_error_library_bad_verdict.json b/tests/fixtures/parity/legacy/errors__http_error_library_bad_verdict.json index 68f0346..0e8a48d 100644 --- a/tests/fixtures/parity/legacy/errors__http_error_library_bad_verdict.json +++ b/tests/fixtures/parity/legacy/errors__http_error_library_bad_verdict.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Invalid verdict label(s). Allowed: False, Mostly False, Mixed, Mostly True, True" } } diff --git a/tests/fixtures/parity/legacy/errors__method_not_allowed_get_verify.json b/tests/fixtures/parity/legacy/errors__method_not_allowed_get_verify.json new file mode 100644 index 0000000..a769f68 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__method_not_allowed_get_verify.json @@ -0,0 +1,11 @@ +{ + "status": 405, + "body": { + "": "Method not allowed" + }, + "headers": { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__method_not_allowed_post_library.json b/tests/fixtures/parity/legacy/errors__method_not_allowed_post_library.json new file mode 100644 index 0000000..a769f68 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__method_not_allowed_post_library.json @@ -0,0 +1,11 @@ +{ + "status": 405, + "body": { + "": "Method not allowed" + }, + "headers": { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__not_found_ask.json b/tests/fixtures/parity/legacy/errors__not_found_ask.json index c0d18e1..65ff442 100644 --- a/tests/fixtures/parity/legacy/errors__not_found_ask.json +++ b/tests/fixtures/parity/legacy/errors__not_found_ask.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Verification not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Verification not found." } } diff --git a/tests/fixtures/parity/legacy/errors__not_found_route.json b/tests/fixtures/parity/legacy/errors__not_found_route.json index 06110be..a16648d 100644 --- a/tests/fixtures/parity/legacy/errors__not_found_route.json +++ b/tests/fixtures/parity/legacy/errors__not_found_route.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "": "" + }, "headers": { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "": "" } } diff --git a/tests/fixtures/parity/legacy/errors__not_found_verification.json b/tests/fixtures/parity/legacy/errors__not_found_verification.json index 3bf3204..538732c 100644 --- a/tests/fixtures/parity/legacy/errors__not_found_verification.json +++ b/tests/fixtures/parity/legacy/errors__not_found_verification.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Not found." } } diff --git a/tests/fixtures/parity/legacy/errors__not_found_verify_status.json b/tests/fixtures/parity/legacy/errors__not_found_verify_status.json index 09c1e79..d005db5 100644 --- a/tests/fixtures/parity/legacy/errors__not_found_verify_status.json +++ b/tests/fixtures/parity/legacy/errors__not_found_verify_status.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Task not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Task not found." } } diff --git a/tests/fixtures/parity/legacy/errors__oauth_register_disabled.json b/tests/fixtures/parity/legacy/errors__oauth_register_disabled.json new file mode 100644 index 0000000..a16648d --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__oauth_register_disabled.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "body": { + "": "" + }, + "headers": { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__oauth_register_invalid_client_metadata.json b/tests/fixtures/parity/legacy/errors__oauth_register_invalid_client_metadata.json new file mode 100644 index 0000000..461c91d --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__oauth_register_invalid_client_metadata.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "body": { + "error": "invalid_client_metadata", + "error_description": "token_endpoint_auth_method must be none: Lenz registers public clients that use PKCE." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__oauth_register_invalid_redirect_uri.json b/tests/fixtures/parity/legacy/errors__oauth_register_invalid_redirect_uri.json new file mode 100644 index 0000000..63e62ba --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__oauth_register_invalid_redirect_uri.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "body": { + "error": "invalid_redirect_uri", + "error_description": "Each redirect URI must be https, or http on localhost, 127.0.0.1 or [::1], without a fragment." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__oauth_revoke_invalid_client.json b/tests/fixtures/parity/legacy/errors__oauth_revoke_invalid_client.json new file mode 100644 index 0000000..a7a70ef --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__oauth_revoke_invalid_client.json @@ -0,0 +1,13 @@ +{ + "status": 401, + "body": { + "error": "invalid_client", + "error_description": "Client authentication failed." + }, + "headers": { + "Content-Type": "application/json", + "WWW-Authenticate": "Basic realm=\"lenz\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__oauth_token_invalid_client.json b/tests/fixtures/parity/legacy/errors__oauth_token_invalid_client.json new file mode 100644 index 0000000..a7a70ef --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__oauth_token_invalid_client.json @@ -0,0 +1,13 @@ +{ + "status": 401, + "body": { + "error": "invalid_client", + "error_description": "Client authentication failed." + }, + "headers": { + "Content-Type": "application/json", + "WWW-Authenticate": "Basic realm=\"lenz\"", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__oauth_token_invalid_grant.json b/tests/fixtures/parity/legacy/errors__oauth_token_invalid_grant.json new file mode 100644 index 0000000..88c6f64 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__oauth_token_invalid_grant.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "body": { + "error": "invalid_grant", + "error_description": "The authorization is invalid or has expired." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__oauth_token_missing_grant_type.json b/tests/fixtures/parity/legacy/errors__oauth_token_missing_grant_type.json new file mode 100644 index 0000000..f989e00 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__oauth_token_missing_grant_type.json @@ -0,0 +1,11 @@ +{ + "status": 400, + "body": { + "error": "unsupported_grant_type" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__oauth_token_refresh_invalid_grant.json b/tests/fixtures/parity/legacy/errors__oauth_token_refresh_invalid_grant.json new file mode 100644 index 0000000..88c6f64 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__oauth_token_refresh_invalid_grant.json @@ -0,0 +1,12 @@ +{ + "status": 400, + "body": { + "error": "invalid_grant", + "error_description": "The authorization is invalid or has expired." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__oauth_token_unsupported_grant_type.json b/tests/fixtures/parity/legacy/errors__oauth_token_unsupported_grant_type.json new file mode 100644 index 0000000..f989e00 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__oauth_token_unsupported_grant_type.json @@ -0,0 +1,11 @@ +{ + "status": 400, + "body": { + "error": "unsupported_grant_type" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/errors__payment_required_ask.json b/tests/fixtures/parity/legacy/errors__payment_required_ask.json index 6648329..cd203cc 100644 --- a/tests/fixtures/parity/legacy/errors__payment_required_ask.json +++ b/tests/fixtures/parity/legacy/errors__payment_required_ask.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining ask credits.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__payment_required_assess.json b/tests/fixtures/parity/legacy/errors__payment_required_assess.json index 1cd3038..50f67b4 100644 --- a/tests/fixtures/parity/legacy/errors__payment_required_assess.json +++ b/tests/fixtures/parity/legacy/errors__payment_required_assess.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining /assess units.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__payment_required_verify.json b/tests/fixtures/parity/legacy/errors__payment_required_verify.json index 32f50ac..bf3a6b1 100644 --- a/tests/fixtures/parity/legacy/errors__payment_required_verify.json +++ b/tests/fixtures/parity/legacy/errors__payment_required_verify.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining claim checks.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 10 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__payment_required_verify_batch_short.json b/tests/fixtures/parity/legacy/errors__payment_required_verify_batch_short.json index 1caec0e..70c05cb 100644 --- a/tests/fixtures/parity/legacy/errors__payment_required_verify_batch_short.json +++ b/tests/fixtures/parity/legacy/errors__payment_required_verify_batch_short.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Insufficient credits for batch.", "code": "no_credits", @@ -13,8 +8,13 @@ "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "requested": 5, "remaining": 3, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 30, "cost": 50 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__rate_limited_extract.json b/tests/fixtures/parity/legacy/errors__rate_limited_extract.json index 9e6149c..1865066 100644 --- a/tests/fixtures/parity/legacy/errors__rate_limited_extract.json +++ b/tests/fixtures/parity/legacy/errors__rate_limited_extract.json @@ -1,11 +1,5 @@ { "status": 429, - "headers": { - "Content-Type": "application/json", - "Retry-After": "900", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", "code": "extract_daily_limit", @@ -13,5 +7,11 @@ "reset_in_seconds": 900, "doc_url": "https://lenz.io/docs/errors#rate-limits", "upgrade_url": "https://lenz.io/plans" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "900", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__service_unavailable_ask.json b/tests/fixtures/parity/legacy/errors__service_unavailable_ask.json index dac95ca..88544c1 100644 --- a/tests/fixtures/parity/legacy/errors__service_unavailable_ask.json +++ b/tests/fixtures/parity/legacy/errors__service_unavailable_ask.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "The chat model is unavailable right now. Retry shortly.", "code": "upstream_unavailable", "retry_after": 90, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__service_unavailable_capacity.json b/tests/fixtures/parity/legacy/errors__service_unavailable_capacity.json index 124f80e..b26cfff 100644 --- a/tests/fixtures/parity/legacy/errors__service_unavailable_capacity.json +++ b/tests/fixtures/parity/legacy/errors__service_unavailable_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "60", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 60, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__validation_malformed_json.json b/tests/fixtures/parity/legacy/errors__validation_malformed_json.json index 0c5de40..4d130b1 100644 --- a/tests/fixtures/parity/legacy/errors__validation_malformed_json.json +++ b/tests/fixtures/parity/legacy/errors__validation_malformed_json.json @@ -1,11 +1,11 @@ { "status": 400, + "body": { + "detail": "Cannot parse request body (Expecting property name enclosed in double quotes: line 1 column 2 (char 1))" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Cannot parse request body (Expecting property name enclosed in double quotes: line 1 column 2 (char 1))" } } diff --git a/tests/fixtures/parity/legacy/errors__validation_missing_field_hint.json b/tests/fixtures/parity/legacy/errors__validation_missing_field_hint.json index 86c632f..b887034 100644 --- a/tests/fixtures/parity/legacy/errors__validation_missing_field_hint.json +++ b/tests/fixtures/parity/legacy/errors__validation_missing_field_hint.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -23,5 +18,10 @@ "content", "bogus" ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__validation_unknown_field_only.json b/tests/fixtures/parity/legacy/errors__validation_unknown_field_only.json index 98527f4..c83cdc2 100644 --- a/tests/fixtures/parity/legacy/errors__validation_unknown_field_only.json +++ b/tests/fixtures/parity/legacy/errors__validation_unknown_field_only.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -22,5 +17,10 @@ "unrecognized_fields": [ "statement" ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__validation_unknown_field_wrong_type.json b/tests/fixtures/parity/legacy/errors__validation_unknown_field_wrong_type.json index 643f307..3aec659 100644 --- a/tests/fixtures/parity/legacy/errors__validation_unknown_field_wrong_type.json +++ b/tests/fixtures/parity/legacy/errors__validation_unknown_field_wrong_type.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -21,5 +16,10 @@ "bogus_field", "another" ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__validation_wrong_type.json b/tests/fixtures/parity/legacy/errors__validation_wrong_type.json index 0c8074f..a2c9bcb 100644 --- a/tests/fixtures/parity/legacy/errors__validation_wrong_type.json +++ b/tests/fixtures/parity/legacy/errors__validation_wrong_type.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -17,5 +12,10 @@ "msg": "Input should be a valid string" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/errors__web_assess_payment_required.json b/tests/fixtures/parity/legacy/errors__web_assess_payment_required.json new file mode 100644 index 0000000..5e8991b --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__web_assess_payment_required.json @@ -0,0 +1,19 @@ +{ + "status": 402, + "body": { + "detail": "No assessments left this month.", + "code": "no_credits", + "doc_url": "https://lenz.io/docs/errors#quota", + "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 0, + "resets_at": "2026-10-01T14:07:00+00:00", + "credits_remaining": 0, + "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "Cookie" + } +} diff --git a/tests/fixtures/parity/legacy/errors__web_assess_signup_required.json b/tests/fixtures/parity/legacy/errors__web_assess_signup_required.json new file mode 100644 index 0000000..7621cf2 --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__web_assess_signup_required.json @@ -0,0 +1,14 @@ +{ + "status": 403, + "body": { + "detail": "Sign up to check this — it’s free.", + "code": "signup_required", + "signup_claim_stashed": false, + "attempt": "e2fffcdbaf5c6cfc1939f3eaec689a87" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "Cookie" + } +} diff --git a/tests/fixtures/parity/legacy/errors__web_review_rate_limited.json b/tests/fixtures/parity/legacy/errors__web_review_rate_limited.json new file mode 100644 index 0000000..0e7609f --- /dev/null +++ b/tests/fixtures/parity/legacy/errors__web_review_rate_limited.json @@ -0,0 +1,17 @@ +{ + "status": 429, + "body": { + "detail": "This account has used its extractions for today. The daily allowance is shared with the Lenz API. It resets at 00:00 UTC.", + "code": "extract_daily_limit", + "limit": 1000, + "reset_in_seconds": 777, + "doc_url": "https://lenz.io/docs/errors#rate-limits", + "upgrade_url": "https://lenz.io/plans" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "777", + "Cache-Control": "no-store", + "Vary": "Cookie" + } +} diff --git a/tests/fixtures/parity/legacy/extract__401_no_key.json b/tests/fixtures/parity/legacy/extract__401_no_key.json index 8dd2fb4..54ec0b6 100644 --- a/tests/fixtures/parity/legacy/extract__401_no_key.json +++ b/tests/fixtures/parity/legacy/extract__401_no_key.json @@ -1,11 +1,11 @@ { "status": 401, + "body": { + "detail": "Unauthorized" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized" } } diff --git a/tests/fixtures/parity/legacy/extract__422_blank_text.json b/tests/fixtures/parity/legacy/extract__422_blank_text.json index 1ebb88e..367f620 100644 --- a/tests/fixtures/parity/legacy/extract__422_blank_text.json +++ b/tests/fixtures/parity/legacy/extract__422_blank_text.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Text is required." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Text is required." } } diff --git a/tests/fixtures/parity/legacy/extract__422_focus_too_long.json b/tests/fixtures/parity/legacy/extract__422_focus_too_long.json index 5804588..2f948a8 100644 --- a/tests/fixtures/parity/legacy/extract__422_focus_too_long.json +++ b/tests/fixtures/parity/legacy/extract__422_focus_too_long.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "focus must be at most 300 characters of plain text." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "focus must be at most 300 characters of plain text." } } diff --git a/tests/fixtures/parity/legacy/extract__422_missing_text.json b/tests/fixtures/parity/legacy/extract__422_missing_text.json index 9e6d6c0..d0c7bfd 100644 --- a/tests/fixtures/parity/legacy/extract__422_missing_text.json +++ b/tests/fixtures/parity/legacy/extract__422_missing_text.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -17,5 +12,10 @@ "msg": "Field required" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__429_daily_limit.json b/tests/fixtures/parity/legacy/extract__429_daily_limit.json index 729effe..1181cbb 100644 --- a/tests/fixtures/parity/legacy/extract__429_daily_limit.json +++ b/tests/fixtures/parity/legacy/extract__429_daily_limit.json @@ -1,11 +1,5 @@ { "status": 429, - "headers": { - "Content-Type": "application/json", - "Retry-After": "3600", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Daily /extract limit of 1000 reached for this account. The allowance is shared by every key on the account and by extractions run from the Lenz website.", "code": "extract_daily_limit", @@ -13,5 +7,11 @@ "reset_in_seconds": 3600, "doc_url": "https://lenz.io/docs/errors#rate-limits", "upgrade_url": "https://lenz.io/plans" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "3600", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__502_extraction_failed.json b/tests/fixtures/parity/legacy/extract__502_extraction_failed.json index 7deef02..529fe27 100644 --- a/tests/fixtures/parity/legacy/extract__502_extraction_failed.json +++ b/tests/fixtures/parity/legacy/extract__502_extraction_failed.json @@ -1,13 +1,13 @@ { "status": 502, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Extraction failed", "code": "extraction_failed", "error": "extraction_failed" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__503_upstream_unavailable.json b/tests/fixtures/parity/legacy/extract__503_upstream_unavailable.json index b308e0c..9308b85 100644 --- a/tests/fixtures/parity/legacy/extract__503_upstream_unavailable.json +++ b/tests/fixtures/parity/legacy/extract__503_upstream_unavailable.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Our model providers are temporarily unavailable. Please retry shortly.", "code": "upstream_unavailable", "retry_after": 90, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__focus_ready.json b/tests/fixtures/parity/legacy/extract__focus_ready.json index 196fca1..2ded502 100644 --- a/tests/fixtures/parity/legacy/extract__focus_ready.json +++ b/tests/fixtures/parity/legacy/extract__focus_ready.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Beta fell 3% last year.", @@ -24,5 +19,10 @@ "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", "locations": null + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__idempotency_body_mismatch_422.json b/tests/fixtures/parity/legacy/extract__idempotency_body_mismatch_422.json index ef04562..2425649 100644 --- a/tests/fixtures/parity/legacy/extract__idempotency_body_mismatch_422.json +++ b/tests/fixtures/parity/legacy/extract__idempotency_body_mismatch_422.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body." } } diff --git a/tests/fixtures/parity/legacy/extract__idempotency_replay.json b/tests/fixtures/parity/legacy/extract__idempotency_replay.json index 8fff1c2..8f23ae8 100644 --- a/tests/fixtures/parity/legacy/extract__idempotency_replay.json +++ b/tests/fixtures/parity/legacy/extract__idempotency_replay.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -27,5 +22,10 @@ "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", "locations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__locate_all_dropped.json b/tests/fixtures/parity/legacy/extract__locate_all_dropped.json index 53117ce..b1365bf 100644 --- a/tests/fixtures/parity/legacy/extract__locate_all_dropped.json +++ b/tests/fixtures/parity/legacy/extract__locate_all_dropped.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "not_a_claim", "claim": "", @@ -24,5 +19,10 @@ "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", "locations": [] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__locate_failed.json b/tests/fixtures/parity/legacy/extract__locate_failed.json index 25c0403..e31d700 100644 --- a/tests/fixtures/parity/legacy/extract__locate_failed.json +++ b/tests/fixtures/parity/legacy/extract__locate_failed.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -27,5 +22,10 @@ "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", "locations": null + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__locate_leading_whitespace.json b/tests/fixtures/parity/legacy/extract__locate_leading_whitespace.json index 7479a3e..b59ad27 100644 --- a/tests/fixtures/parity/legacy/extract__locate_leading_whitespace.json +++ b/tests/fixtures/parity/legacy/extract__locate_leading_whitespace.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -35,5 +30,10 @@ ] } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__locate_some_dropped.json b/tests/fixtures/parity/legacy/extract__locate_some_dropped.json index 6e51827..c059de8 100644 --- a/tests/fixtures/parity/legacy/extract__locate_some_dropped.json +++ b/tests/fixtures/parity/legacy/extract__locate_some_dropped.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -48,5 +43,10 @@ ] } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__locate_true.json b/tests/fixtures/parity/legacy/extract__locate_true.json index 6e51827..c059de8 100644 --- a/tests/fixtures/parity/legacy/extract__locate_true.json +++ b/tests/fixtures/parity/legacy/extract__locate_true.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -48,5 +43,10 @@ ] } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__no_match_with_focus.json b/tests/fixtures/parity/legacy/extract__no_match_with_focus.json index 0b34d79..45eb087 100644 --- a/tests/fixtures/parity/legacy/extract__no_match_with_focus.json +++ b/tests/fixtures/parity/legacy/extract__no_match_with_focus.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "no_match", "claim": "", @@ -15,5 +10,10 @@ "presumed_intent": "", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", "locations": null + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__not_a_claim.json b/tests/fixtures/parity/legacy/extract__not_a_claim.json index 265efee..a9af794 100644 --- a/tests/fixtures/parity/legacy/extract__not_a_claim.json +++ b/tests/fixtures/parity/legacy/extract__not_a_claim.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "not_a_claim", "claim": "", @@ -15,5 +10,10 @@ "presumed_intent": "", "original_input": "What a lovely day.", "locations": null + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__not_a_claim_beside_claims.json b/tests/fixtures/parity/legacy/extract__not_a_claim_beside_claims.json index 97f66d5..4e6718b 100644 --- a/tests/fixtures/parity/legacy/extract__not_a_claim_beside_claims.json +++ b/tests/fixtures/parity/legacy/extract__not_a_claim_beside_claims.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "not_a_claim", "claim": "Alpha rose 5% in 2024.", @@ -24,5 +19,10 @@ "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024.", "locations": null + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__ready_one_claim.json b/tests/fixtures/parity/legacy/extract__ready_one_claim.json index 0a32c8b..1b9b2b6 100644 --- a/tests/fixtures/parity/legacy/extract__ready_one_claim.json +++ b/tests/fixtures/parity/legacy/extract__ready_one_claim.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -24,5 +19,10 @@ "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024.", "locations": null + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__ready_several_claims.json b/tests/fixtures/parity/legacy/extract__ready_several_claims.json index 25c0403..e31d700 100644 --- a/tests/fixtures/parity/legacy/extract__ready_several_claims.json +++ b/tests/fixtures/parity/legacy/extract__ready_several_claims.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -27,5 +22,10 @@ "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", "locations": null + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__replay_later_200.json b/tests/fixtures/parity/legacy/extract__replay_later_200.json index 8fff1c2..8f23ae8 100644 --- a/tests/fixtures/parity/legacy/extract__replay_later_200.json +++ b/tests/fixtures/parity/legacy/extract__replay_later_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -27,5 +22,10 @@ "presumed_intent": "Verify reported figures", "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", "locations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__url_fetch_502_unreadable.json b/tests/fixtures/parity/legacy/extract__url_fetch_502_unreadable.json index 7deef02..529fe27 100644 --- a/tests/fixtures/parity/legacy/extract__url_fetch_502_unreadable.json +++ b/tests/fixtures/parity/legacy/extract__url_fetch_502_unreadable.json @@ -1,13 +1,13 @@ { "status": 502, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Extraction failed", "code": "extraction_failed", "error": "extraction_failed" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__url_fetch_503_upstream.json b/tests/fixtures/parity/legacy/extract__url_fetch_503_upstream.json index 7cbd9f8..3b1746c 100644 --- a/tests/fixtures/parity/legacy/extract__url_fetch_503_upstream.json +++ b/tests/fixtures/parity/legacy/extract__url_fetch_503_upstream.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "The page could not be read right now. Please retry shortly.", "code": "upstream_unavailable", "retry_after": 90, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/extract__url_input_located.json b/tests/fixtures/parity/legacy/extract__url_input_located.json index 2cacb24..8505b1e 100644 --- a/tests/fixtures/parity/legacy/extract__url_input_located.json +++ b/tests/fixtures/parity/legacy/extract__url_input_located.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "ready", "claim": "Alpha rose 5% in 2024.", @@ -35,5 +30,10 @@ ] } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__401_no_credentials.json b/tests/fixtures/parity/legacy/review__401_no_credentials.json index 8dd2fb4..54ec0b6 100644 --- a/tests/fixtures/parity/legacy/review__401_no_credentials.json +++ b/tests/fixtures/parity/legacy/review__401_no_credentials.json @@ -1,11 +1,11 @@ { "status": 401, + "body": { + "detail": "Unauthorized" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized" } } diff --git a/tests/fixtures/parity/legacy/review__402_no_credits.json b/tests/fixtures/parity/legacy/review__402_no_credits.json index c9adf9a..8fd4d67 100644 --- a/tests/fixtures/parity/legacy/review__402_no_credits.json +++ b/tests/fixtures/parity/legacy/review__402_no_credits.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining credits to assess the draft.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 100, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 100, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__402_no_credits_exhausted.json b/tests/fixtures/parity/legacy/review__402_no_credits_exhausted.json index cbcd2d1..591f328 100644 --- a/tests/fixtures/parity/legacy/review__402_no_credits_exhausted.json +++ b/tests/fixtures/parity/legacy/review__402_no_credits_exhausted.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining credits to assess the draft.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 1 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_blank_text.json b/tests/fixtures/parity/legacy/review__422_blank_text.json index b820499..b62d04a 100644 --- a/tests/fixtures/parity/legacy/review__422_blank_text.json +++ b/tests/fixtures/parity/legacy/review__422_blank_text.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "text: send the draft, or one public http(s) URL.", "code": "validation_error", @@ -17,5 +12,10 @@ "msg": "text: send the draft, or one public http(s) URL." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_citations_options_object.json b/tests/fixtures/parity/legacy/review__422_citations_options_object.json index 1f3923f..a09435f 100644 --- a/tests/fixtures/parity/legacy/review__422_citations_options_object.json +++ b/tests/fixtures/parity/legacy/review__422_citations_options_object.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.citations: Extra inputs are not permitted", "code": "validation_error", @@ -18,5 +13,10 @@ "msg": "Extra inputs are not permitted" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_depth_unknown.json b/tests/fixtures/parity/legacy/review__422_depth_unknown.json index dcce2ae..e2c2889 100644 --- a/tests/fixtures/parity/legacy/review__422_depth_unknown.json +++ b/tests/fixtures/parity/legacy/review__422_depth_unknown.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.escalate.depth: Input should be 'standard' or 'low'", "code": "validation_error", @@ -19,5 +14,10 @@ "msg": "Input should be 'standard' or 'low'" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_invalid_confidence_band.json b/tests/fixtures/parity/legacy/review__422_invalid_confidence_band.json index 8dccb97..32e16e2 100644 --- a/tests/fixtures/parity/legacy/review__422_invalid_confidence_band.json +++ b/tests/fixtures/parity/legacy/review__422_invalid_confidence_band.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.escalate.confidence: Unknown confidence band 'certain'; use low, medium or high.", "code": "invalid_confidence_band", @@ -19,5 +14,10 @@ "msg": "Unknown confidence band 'certain'; use low, medium or high." } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_invalid_verdict_label.json b/tests/fixtures/parity/legacy/review__422_invalid_verdict_label.json index 40f409f..320e56a 100644 --- a/tests/fixtures/parity/legacy/review__422_invalid_verdict_label.json +++ b/tests/fixtures/parity/legacy/review__422_invalid_verdict_label.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.escalate.verdicts: Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True.", "code": "invalid_verdict_label", @@ -19,5 +14,10 @@ "msg": "Unknown verdict label 'Misleading'; use one of: False, Mostly False, Mixed, Mostly True, True." } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_max_assessments_negative.json b/tests/fixtures/parity/legacy/review__422_max_assessments_negative.json index c88a65e..0715227 100644 --- a/tests/fixtures/parity/legacy/review__422_max_assessments_negative.json +++ b/tests/fixtures/parity/legacy/review__422_max_assessments_negative.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.escalate.max_assessments: Input should be greater than or equal to 0", "code": "validation_error", @@ -19,5 +14,10 @@ "msg": "Input should be greater than or equal to 0" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_max_citations_negative.json b/tests/fixtures/parity/legacy/review__422_max_citations_negative.json index 13e166c..46dc9ee 100644 --- a/tests/fixtures/parity/legacy/review__422_max_citations_negative.json +++ b/tests/fixtures/parity/legacy/review__422_max_citations_negative.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.escalate.max_citations: Input should be greater than or equal to 0", "code": "validation_error", @@ -19,5 +14,10 @@ "msg": "Input should be greater than or equal to 0" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_max_citations_too_big.json b/tests/fixtures/parity/legacy/review__422_max_citations_too_big.json index 10fcf2c..7ec1805 100644 --- a/tests/fixtures/parity/legacy/review__422_max_citations_too_big.json +++ b/tests/fixtures/parity/legacy/review__422_max_citations_too_big.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.escalate.max_citations: Input should be less than or equal to 20", "code": "validation_error", @@ -19,5 +14,10 @@ "msg": "Input should be less than or equal to 20" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_max_verifications_too_big.json b/tests/fixtures/parity/legacy/review__422_max_verifications_too_big.json index 9c2e2eb..c5c773b 100644 --- a/tests/fixtures/parity/legacy/review__422_max_verifications_too_big.json +++ b/tests/fixtures/parity/legacy/review__422_max_verifications_too_big.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.escalate.max_verifications: Input should be less than or equal to 20", "code": "validation_error", @@ -19,5 +14,10 @@ "msg": "Input should be less than or equal to 20" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_text_missing.json b/tests/fixtures/parity/legacy/review__422_text_missing.json index de972a9..8380243 100644 --- a/tests/fixtures/parity/legacy/review__422_text_missing.json +++ b/tests/fixtures/parity/legacy/review__422_text_missing.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.text: Field required", "code": "validation_error", @@ -18,5 +13,10 @@ "msg": "Field required" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_text_wrong_type.json b/tests/fixtures/parity/legacy/review__422_text_wrong_type.json index 2638c63..696b78e 100644 --- a/tests/fixtures/parity/legacy/review__422_text_wrong_type.json +++ b/tests/fixtures/parity/legacy/review__422_text_wrong_type.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.text: Input should be a valid string", "code": "validation_error", @@ -18,5 +13,10 @@ "msg": "Input should be a valid string" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_unknown_escalate_field.json b/tests/fixtures/parity/legacy/review__422_unknown_escalate_field.json index 2c5c7e9..d188f40 100644 --- a/tests/fixtures/parity/legacy/review__422_unknown_escalate_field.json +++ b/tests/fixtures/parity/legacy/review__422_unknown_escalate_field.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.escalate.nope: Extra inputs are not permitted", "code": "validation_error", @@ -19,5 +14,10 @@ "msg": "Extra inputs are not permitted" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_unknown_top_level_field.json b/tests/fixtures/parity/legacy/review__422_unknown_top_level_field.json index f91e1f3..ea96224 100644 --- a/tests/fixtures/parity/legacy/review__422_unknown_top_level_field.json +++ b/tests/fixtures/parity/legacy/review__422_unknown_top_level_field.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.max_verifications: Extra inputs are not permitted", "code": "validation_error", @@ -18,5 +13,10 @@ "msg": "Extra inputs are not permitted" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_unsupported_language.json b/tests/fixtures/parity/legacy/review__422_unsupported_language.json index 26f0a19..ef3d68b 100644 --- a/tests/fixtures/parity/legacy/review__422_unsupported_language.json +++ b/tests/fixtures/parity/legacy/review__422_unsupported_language.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "language: Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact.", "code": "validation_error", @@ -17,5 +12,10 @@ "msg": "language: Unsupported language 'xx'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_visibility_public.json b/tests/fixtures/parity/legacy/review__422_visibility_public.json index 3c72623..3b923ea 100644 --- a/tests/fixtures/parity/legacy/review__422_visibility_public.json +++ b/tests/fixtures/parity/legacy/review__422_visibility_public.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "payload.visibility: Input should be 'private' or 'unlisted'", "code": "validation_error", @@ -18,5 +13,10 @@ "msg": "Input should be 'private' or 'unlisted'" } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__422_webhook_secret_missing.json b/tests/fixtures/parity/legacy/review__422_webhook_secret_missing.json new file mode 100644 index 0000000..1ecf2d1 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__422_webhook_secret_missing.json @@ -0,0 +1,21 @@ +{ + "status": 422, + "body": { + "detail": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend.", + "code": "webhook_secret_missing", + "errors": [ + { + "loc": [ + "body", + "webhook_url" + ], + "msg": "webhook_url was supplied but this credential has no webhook signing secret. Create one on the API credentials page (an API key) or read GET /me/webhook-secret (an OAuth connection), then resend." + } + ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/review__429_extract_daily_limit.json b/tests/fixtures/parity/legacy/review__429_extract_daily_limit.json index f7f182a..1bc25ba 100644 --- a/tests/fixtures/parity/legacy/review__429_extract_daily_limit.json +++ b/tests/fixtures/parity/legacy/review__429_extract_daily_limit.json @@ -1,11 +1,5 @@ { "status": 429, - "headers": { - "Content-Type": "application/json", - "Retry-After": "60", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Daily /extract limit of 1000 reached for this account. A review of a URL reads the page like /extract and counts as one /extract call.", "code": "extract_daily_limit", @@ -13,5 +7,11 @@ "reset_in_seconds": 60, "doc_url": "https://lenz.io/docs/errors#rate-limits", "upgrade_url": "https://lenz.io/plans" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "60", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__429_review_in_flight.json b/tests/fixtures/parity/legacy/review__429_review_in_flight.json index c8777a5..2129d2a 100644 --- a/tests/fixtures/parity/legacy/review__429_review_in_flight.json +++ b/tests/fixtures/parity/legacy/review__429_review_in_flight.json @@ -1,14 +1,14 @@ { "status": 429, + "body": { + "detail": "This account already has 3 reviews running. Retry when one completes.", + "code": "review_in_flight", + "retry_after_seconds": 60 + }, "headers": { "Content-Type": "application/json", "Retry-After": "60", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "This account already has 3 reviews running. Retry when one completes.", - "code": "review_in_flight", - "retry_after_seconds": 60 } } diff --git a/tests/fixtures/parity/legacy/review__503_capacity.json b/tests/fixtures/parity/legacy/review__503_capacity.json index adcc209..085990c 100644 --- a/tests/fixtures/parity/legacy/review__503_capacity.json +++ b/tests/fixtures/parity/legacy/review__503_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 90, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__503_citations_capacity.json b/tests/fixtures/parity/legacy/review__503_citations_capacity.json index adcc209..085990c 100644 --- a/tests/fixtures/parity/legacy/review__503_citations_capacity.json +++ b/tests/fixtures/parity/legacy/review__503_citations_capacity.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "90", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": 90, "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "90", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__delete_not_a_route.json b/tests/fixtures/parity/legacy/review__delete_not_a_route.json new file mode 100644 index 0000000..a769f68 --- /dev/null +++ b/tests/fixtures/parity/legacy/review__delete_not_a_route.json @@ -0,0 +1,11 @@ +{ + "status": 405, + "body": { + "": "Method not allowed" + }, + "headers": { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/review__get_403_insufficient_scope.json b/tests/fixtures/parity/legacy/review__get_403_insufficient_scope.json index 80c529c..b6b8f4f 100644 --- a/tests/fixtures/parity/legacy/review__get_403_insufficient_scope.json +++ b/tests/fixtures/parity/legacy/review__get_403_insufficient_scope.json @@ -1,14 +1,14 @@ { "status": 403, + "body": { + "detail": "The access token does not allow this call.", + "code": "insufficient_scope", + "scope": "history:read" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "WWW-Authenticate": "Bearer error=\"insufficient_scope\", scope=\"history:read\", resource_metadata=\"https://lenz.io/.well-known/oauth-protected-resource/api/v1\"", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "The access token does not allow this call.", - "code": "insufficient_scope", - "scope": "history:read" } } diff --git a/tests/fixtures/parity/legacy/review__get_404_not_found.json b/tests/fixtures/parity/legacy/review__get_404_not_found.json index 8045cd8..e73d347 100644 --- a/tests/fixtures/parity/legacy/review__get_404_not_found.json +++ b/tests/fixtures/parity/legacy/review__get_404_not_found.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Review not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Review not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/legacy/review__get_404_other_account.json b/tests/fixtures/parity/legacy/review__get_404_other_account.json index 8045cd8..e73d347 100644 --- a/tests/fixtures/parity/legacy/review__get_404_other_account.json +++ b/tests/fixtures/parity/legacy/review__get_404_other_account.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Review not found.", + "code": "not_found" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Review not found.", - "code": "not_found" } } diff --git a/tests/fixtures/parity/legacy/review__get_410_purged.json b/tests/fixtures/parity/legacy/review__get_410_purged.json index bedf6a5..396b15a 100644 --- a/tests/fixtures/parity/legacy/review__get_410_purged.json +++ b/tests/fixtures/parity/legacy/review__get_410_purged.json @@ -1,13 +1,13 @@ { "status": 410, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This review is no longer available: its account removes content after a set period.", "code": "purged", "purged_at": "2026-10-01T09:07:00.123456Z" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_422_unknown_view.json b/tests/fixtures/parity/legacy/review__get_422_unknown_view.json index 531cbd7..c73271c 100644 --- a/tests/fixtures/parity/legacy/review__get_422_unknown_view.json +++ b/tests/fixtures/parity/legacy/review__get_422_unknown_view.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "view: use full or issues.", "code": "validation_error", @@ -17,5 +12,10 @@ "msg": "view: use full or issues." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_assessing.json b/tests/fixtures/parity/legacy/review__get_assessing.json index eff0c0d..15f9bb2 100644 --- a/tests/fixtures/parity/legacy/review__get_assessing.json +++ b/tests/fixtures/parity/legacy/review__get_assessing.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "assessing", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -106,5 +101,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_assessment_rows_full_fields.json b/tests/fixtures/parity/legacy/review__get_assessment_rows_full_fields.json index c7b40ba..a63744a 100644 --- a/tests/fixtures/parity/legacy/review__get_assessment_rows_full_fields.json +++ b/tests/fixtures/parity/legacy/review__get_assessment_rows_full_fields.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -166,5 +161,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_clean.json b/tests/fixtures/parity/legacy/review__get_citations_clean.json index c4bb6cc..8b713d8 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_clean.json +++ b/tests/fixtures/parity/legacy/review__get_citations_clean.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -179,5 +174,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_failed_and_unchecked.json b/tests/fixtures/parity/legacy/review__get_citations_failed_and_unchecked.json index 39737a0..3d74122 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_failed_and_unchecked.json +++ b/tests/fixtures/parity/legacy/review__get_citations_failed_and_unchecked.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -195,5 +190,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_issue_full.json b/tests/fixtures/parity/legacy/review__get_citations_issue_full.json index df60b05..90333a3 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_issue_full.json +++ b/tests/fixtures/parity/legacy/review__get_citations_issue_full.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -201,5 +196,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_issue_issues_view.json b/tests/fixtures/parity/legacy/review__get_citations_issue_issues_view.json index c8414d9..676571c 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_issue_issues_view.json +++ b/tests/fixtures/parity/legacy/review__get_citations_issue_issues_view.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "issues", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -89,5 +84,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_limit_exactly_at_limit.json b/tests/fixtures/parity/legacy/review__get_citations_limit_exactly_at_limit.json index 9d9e9d7..117f684 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_limit_exactly_at_limit.json +++ b/tests/fixtures/parity/legacy/review__get_citations_limit_exactly_at_limit.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -179,5 +174,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_limit_reached.json b/tests/fixtures/parity/legacy/review__get_citations_limit_reached.json index 570c334..e4dd5cd 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_limit_reached.json +++ b/tests/fixtures/parity/legacy/review__get_citations_limit_reached.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -153,5 +148,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_no_claim_but_citations.json b/tests/fixtures/parity/legacy/review__get_citations_no_claim_but_citations.json index 5c19752..3025e57 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_no_claim_but_citations.json +++ b/tests/fixtures/parity/legacy/review__get_citations_no_claim_but_citations.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -148,5 +143,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_no_claim_no_citation.json b/tests/fixtures/parity/legacy/review__get_citations_no_claim_no_citation.json index c588505..eca6db3 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_no_claim_no_citation.json +++ b/tests/fixtures/parity/legacy/review__get_citations_no_claim_no_citation.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -64,5 +59,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_not_asked.json b/tests/fixtures/parity/legacy/review__get_citations_not_asked.json index 386fe10..f6afdaa 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_not_asked.json +++ b/tests/fixtures/parity/legacy/review__get_citations_not_asked.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -96,5 +91,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_only.json b/tests/fixtures/parity/legacy/review__get_citations_only.json index 287e33e..c5863b1 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_only.json +++ b/tests/fixtures/parity/legacy/review__get_citations_only.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -155,5 +150,10 @@ } ], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_partly_supported.json b/tests/fixtures/parity/legacy/review__get_citations_partly_supported.json index 6c96719..51ae0f4 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_partly_supported.json +++ b/tests/fixtures/parity/legacy/review__get_citations_partly_supported.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -179,5 +174,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_running.json b/tests/fixtures/parity/legacy/review__get_citations_running.json index e72b98c..b7ea324 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_running.json +++ b/tests/fixtures/parity/legacy/review__get_citations_running.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "verifying", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -163,5 +158,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_skipped_switched_off.json b/tests/fixtures/parity/legacy/review__get_citations_skipped_switched_off.json index b67e879..23514b6 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_skipped_switched_off.json +++ b/tests/fixtures/parity/legacy/review__get_citations_skipped_switched_off.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -96,5 +91,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_citations_skipped_url_input.json b/tests/fixtures/parity/legacy/review__get_citations_skipped_url_input.json index 04d13c0..976d0d6 100644 --- a/tests/fixtures/parity/legacy/review__get_citations_skipped_url_input.json +++ b/tests/fixtures/parity/legacy/review__get_citations_skipped_url_input.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -64,5 +59,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_claim_limit_not_reached.json b/tests/fixtures/parity/legacy/review__get_claim_limit_not_reached.json index 97b6112..3e0cb66 100644 --- a/tests/fixtures/parity/legacy/review__get_claim_limit_not_reached.json +++ b/tests/fixtures/parity/legacy/review__get_claim_limit_not_reached.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -126,5 +121,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_claim_limit_reached_exact.json b/tests/fixtures/parity/legacy/review__get_claim_limit_reached_exact.json index 55d5182..d044437 100644 --- a/tests/fixtures/parity/legacy/review__get_claim_limit_reached_exact.json +++ b/tests/fixtures/parity/legacy/review__get_claim_limit_reached_exact.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -126,5 +121,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_completed_assess_only.json b/tests/fixtures/parity/legacy/review__get_completed_assess_only.json index ff8ce84..da57c84 100644 --- a/tests/fixtures/parity/legacy/review__get_completed_assess_only.json +++ b/tests/fixtures/parity/legacy/review__get_completed_assess_only.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -152,5 +147,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_completed_clean.json b/tests/fixtures/parity/legacy/review__get_completed_clean.json index 3f2020c..6c37fea 100644 --- a/tests/fixtures/parity/legacy/review__get_completed_clean.json +++ b/tests/fixtures/parity/legacy/review__get_completed_clean.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -126,5 +121,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_completed_issues_all_verified.json b/tests/fixtures/parity/legacy/review__get_completed_issues_all_verified.json index 81957e3..03453bf 100644 --- a/tests/fixtures/parity/legacy/review__get_completed_issues_all_verified.json +++ b/tests/fixtures/parity/legacy/review__get_completed_issues_all_verified.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -63,10 +58,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -86,10 +81,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b79019e4", + "verification_id": "4d44dfaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", - "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "verification_url": "https://lenz.io/api/v1/verifications/4d44dfaf", + "url": "https://lenz.io/c/the-earth-is-round-4d44dfaf", "escalation": { "matched_rules": [ "verdict" @@ -139,8 +134,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "c010dca9a43543f518e49f4ec7842ceb", - "verification_id": "b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The EU AI Act took effect in March 2024.", "language": "en", "visibility": "private", @@ -156,10 +151,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:49:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "modified_at": null, - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -196,8 +191,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "4634b89821fd877f1edf8aaeb82b7ee5", - "verification_id": "b79019e4", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "verification_id": "4d44dfaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -213,10 +208,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:49:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "modified_at": null, - "verification_url": "https://lenz.io/api/v1/verifications/b79019e4", - "url": "https://lenz.io/c/the-earth-is-round-b79019e4", + "verification_url": "https://lenz.io/api/v1/verifications/4d44dfaf", + "url": "https://lenz.io/c/the-earth-is-round-4d44dfaf", "failure": null }, "suggested_edits": null @@ -257,5 +252,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_completed_issues_full.json b/tests/fixtures/parity/legacy/review__get_completed_issues_full.json index a8c7245..7f38ed6 100644 --- a/tests/fixtures/parity/legacy/review__get_completed_issues_full.json +++ b/tests/fixtures/parity/legacy/review__get_completed_issues_full.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -63,10 +58,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -158,8 +153,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "7df4009bc27aab64ce5d5a01fbafe599", - "verification_id": "b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The EU AI Act took effect in March 2024.", "language": "en", "visibility": "private", @@ -175,10 +170,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:42:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "modified_at": null, - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -242,7 +237,7 @@ }, "verification": { "verification_id": null, - "task_id": "5ccac77f53ff49328ec134fb29d66542", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", "claim": null, "language": null, "visibility": null, @@ -308,5 +303,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_completed_issues_view_issues.json b/tests/fixtures/parity/legacy/review__get_completed_issues_view_issues.json index c42387e..c6f0f7d 100644 --- a/tests/fixtures/parity/legacy/review__get_completed_issues_view_issues.json +++ b/tests/fixtures/parity/legacy/review__get_completed_issues_view_issues.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "issues", "status": "completed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -63,10 +58,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -129,5 +124,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_completed_positions.json b/tests/fixtures/parity/legacy/review__get_completed_positions.json index 85f1f60..be3456b 100644 --- a/tests/fixtures/parity/legacy/review__get_completed_positions.json +++ b/tests/fixtures/parity/legacy/review__get_completed_positions.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -138,5 +133,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_completed_recovered_claim.json b/tests/fixtures/parity/legacy/review__get_completed_recovered_claim.json index 9bba88a..5320c8c 100644 --- a/tests/fixtures/parity/legacy/review__get_completed_recovered_claim.json +++ b/tests/fixtures/parity/legacy/review__get_completed_recovered_claim.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -96,5 +91,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_completed_with_untraced_claim.json b/tests/fixtures/parity/legacy/review__get_completed_with_untraced_claim.json index c58b50b..b46dc3c 100644 --- a/tests/fixtures/parity/legacy/review__get_completed_with_untraced_claim.json +++ b/tests/fixtures/parity/legacy/review__get_completed_with_untraced_claim.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -138,5 +133,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_during_quick_check.json b/tests/fixtures/parity/legacy/review__get_during_quick_check.json index 7b3787b..967b16c 100644 --- a/tests/fixtures/parity/legacy/review__get_during_quick_check.json +++ b/tests/fixtures/parity/legacy/review__get_during_quick_check.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "assessing", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -84,5 +79,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_failed_every_assessment_failed.json b/tests/fixtures/parity/legacy/review__get_failed_every_assessment_failed.json index 12698ba..d117ab8 100644 --- a/tests/fixtures/parity/legacy/review__get_failed_every_assessment_failed.json +++ b/tests/fixtures/parity/legacy/review__get_failed_every_assessment_failed.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "incomplete", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -153,5 +148,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_failed_no_claim.json b/tests/fixtures/parity/legacy/review__get_failed_no_claim.json index 5d5200a..4a274ab 100644 --- a/tests/fixtures/parity/legacy/review__get_failed_no_claim.json +++ b/tests/fixtures/parity/legacy/review__get_failed_no_claim.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -64,5 +59,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_failed_no_claim_untraced.json b/tests/fixtures/parity/legacy/review__get_failed_no_claim_untraced.json index 823468b..afdea14 100644 --- a/tests/fixtures/parity/legacy/review__get_failed_no_claim_untraced.json +++ b/tests/fixtures/parity/legacy/review__get_failed_no_claim_untraced.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -64,5 +59,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_more_claims.json b/tests/fixtures/parity/legacy/review__get_more_claims.json index 5f268da..fc21fb8 100644 --- a/tests/fixtures/parity/legacy/review__get_more_claims.json +++ b/tests/fixtures/parity/legacy/review__get_more_claims.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -151,5 +146,10 @@ } ], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json index 52e321f..b3c18bb 100644 --- a/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -63,10 +58,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -116,8 +111,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "7df4009bc27aab64ce5d5a01fbafe599", - "verification_id": "b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -135,8 +130,8 @@ ], "created_at": "2026-10-01T23:55:00.123456+00:00", "modified_at": "2026-10-02T00:03:00.123456+00:00", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -147,5 +142,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_same_day_hours_apart.json index 9054ce3..12724a6 100644 --- a/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/legacy/review__get_nested_verification_modified_at_same_day_hours_apart.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -63,10 +58,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "b4e2e99d", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -116,8 +111,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "7df4009bc27aab64ce5d5a01fbafe599", - "verification_id": "b4e2e99d", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -135,8 +130,8 @@ ], "created_at": "2026-10-01T08:00:00.123456+00:00", "modified_at": null, - "verification_url": "https://lenz.io/api/v1/verifications/b4e2e99d", - "url": "https://lenz.io/c/the-earth-is-round-b4e2e99d", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -147,5 +142,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_policy_resolved.json b/tests/fixtures/parity/legacy/review__get_policy_resolved.json index 3116b84..20bfc3f 100644 --- a/tests/fixtures/parity/legacy/review__get_policy_resolved.json +++ b/tests/fixtures/parity/legacy/review__get_policy_resolved.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -94,5 +89,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_queued.json b/tests/fixtures/parity/legacy/review__get_queued.json index 306403b..7e2880c 100644 --- a/tests/fixtures/parity/legacy/review__get_queued.json +++ b/tests/fixtures/parity/legacy/review__get_queued.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "queued", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -58,5 +53,10 @@ "more_claims": null, "more_claim_locations": null, "more_citations": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_suggested_edits.json b/tests/fixtures/parity/legacy/review__get_suggested_edits.json index aba4758..159dab2 100644 --- a/tests/fixtures/parity/legacy/review__get_suggested_edits.json +++ b/tests/fixtures/parity/legacy/review__get_suggested_edits.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "verifying", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -63,10 +58,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "7c83247c", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", - "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -159,8 +154,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "9bca1e0819de60a750186e812dc58e2b", - "verification_id": "7c83247c", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -176,10 +171,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:35:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "modified_at": null, - "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", - "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": { @@ -265,5 +260,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_suggested_edits_issues_view.json b/tests/fixtures/parity/legacy/review__get_suggested_edits_issues_view.json index 1ffd8c0..a3f8882 100644 --- a/tests/fixtures/parity/legacy/review__get_suggested_edits_issues_view.json +++ b/tests/fixtures/parity/legacy/review__get_suggested_edits_issues_view.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "issues", "status": "verifying", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -63,10 +58,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "7c83247c", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/7c83247c", - "url": "https://lenz.io/c/the-earth-is-round-7c83247c", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -124,5 +119,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_url_review.json b/tests/fixtures/parity/legacy/review__get_url_review.json index ca8ddf6..cbc4a81 100644 --- a/tests/fixtures/parity/legacy/review__get_url_review.json +++ b/tests/fixtures/parity/legacy/review__get_url_review.json @@ -1,17 +1,12 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:14:00.123456Z", - "completed_at": "2026-10-01T09:21:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -102,5 +97,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__get_verifying.json b/tests/fixtures/parity/legacy/review__get_verifying.json index a388043..39f630e 100644 --- a/tests/fixtures/parity/legacy/review__get_verifying.json +++ b/tests/fixtures/parity/legacy/review__get_verifying.json @@ -1,16 +1,11 @@ { "status": 200, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "review_id": "d6b2bd72", "view": "full", "status": "verifying", "outcome": null, - "created_at": "2026-10-01T09:14:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", "completed_at": null, "language": "en", "policy": { @@ -175,5 +170,10 @@ "more_claims": [], "more_claim_locations": [], "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__idempotency_body_mismatch_422.json b/tests/fixtures/parity/legacy/review__idempotency_body_mismatch_422.json index fc47586..1c526f0 100644 --- a/tests/fixtures/parity/legacy/review__idempotency_body_mismatch_422.json +++ b/tests/fixtures/parity/legacy/review__idempotency_body_mismatch_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Idempotency-Key reused with a different request body.", "code": "idempotency_body_mismatch", @@ -16,5 +11,10 @@ "msg": "Idempotency-Key reused with a different request body." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__idempotency_conflict_409.json b/tests/fixtures/parity/legacy/review__idempotency_conflict_409.json index 8bbdff7..e0a63b2 100644 --- a/tests/fixtures/parity/legacy/review__idempotency_conflict_409.json +++ b/tests/fixtures/parity/legacy/review__idempotency_conflict_409.json @@ -1,13 +1,13 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "A review with this Idempotency-Key is still being created. Retry shortly.", "code": "idempotency_conflict", "review_id": null + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__idempotency_conflict_409_existing_review.json b/tests/fixtures/parity/legacy/review__idempotency_conflict_409_existing_review.json index edef440..e91fe17 100644 --- a/tests/fixtures/parity/legacy/review__idempotency_conflict_409_existing_review.json +++ b/tests/fixtures/parity/legacy/review__idempotency_conflict_409_existing_review.json @@ -1,13 +1,13 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "A review with this Idempotency-Key is still being created. Retry shortly.", "code": "idempotency_conflict", "review_id": "d6b2bd72" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/review__idempotent_replay_202.json b/tests/fixtures/parity/legacy/review__idempotent_replay_202.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/legacy/review__idempotent_replay_202.json +++ b/tests/fixtures/parity/legacy/review__idempotent_replay_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/legacy/review__receipt_202.json b/tests/fixtures/parity/legacy/review__receipt_202.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/legacy/review__receipt_202.json +++ b/tests/fixtures/parity/legacy/review__receipt_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/legacy/review__receipt_202_empty_webhook_url.json b/tests/fixtures/parity/legacy/review__receipt_202_empty_webhook_url.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/legacy/review__receipt_202_empty_webhook_url.json +++ b/tests/fixtures/parity/legacy/review__receipt_202_empty_webhook_url.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/legacy/review__receipt_202_webhook_url.json b/tests/fixtures/parity/legacy/review__receipt_202_webhook_url.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/legacy/review__receipt_202_webhook_url.json +++ b/tests/fixtures/parity/legacy/review__receipt_202_webhook_url.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/legacy/review__replay_later_202.json b/tests/fixtures/parity/legacy/review__replay_later_202.json index 6a0872b..ea99cf8 100644 --- a/tests/fixtures/parity/legacy/review__replay_later_202.json +++ b/tests/fixtures/parity/legacy/review__replay_later_202.json @@ -1,14 +1,14 @@ { "status": 202, + "body": { + "review_id": "d6b2bd72", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Retry-After": "20", "Location": "/api/v1/reviews/d6b2bd72", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "review_id": "d6b2bd72", - "status": "queued" } } diff --git a/tests/fixtures/parity/legacy/verify__batch_202.json b/tests/fixtures/parity/legacy/verify__batch_202.json index 7229ce4..f35e0ac 100644 --- a/tests/fixtures/parity/legacy/verify__batch_202.json +++ b/tests/fixtures/parity/legacy/verify__batch_202.json @@ -1,21 +1,21 @@ { "status": 202, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "batch_id": "7ea9cdbd4064e3cf", "items": [ { - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "claim_text": "The Earth is round." }, { - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", "claim_text": "Water boils at 100C at sea level." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__batch_capacity_503.json b/tests/fixtures/parity/legacy/verify__batch_capacity_503.json index 342c8aa..ea99c5b 100644 --- a/tests/fixtures/parity/legacy/verify__batch_capacity_503.json +++ b/tests/fixtures/parity/legacy/verify__batch_capacity_503.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": "", "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__batch_empty_422.json b/tests/fixtures/parity/legacy/verify__batch_empty_422.json index 48a686f..8cda82f 100644 --- a/tests/fixtures/parity/legacy/verify__batch_empty_422.json +++ b/tests/fixtures/parity/legacy/verify__batch_empty_422.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "claims is required and must be non-empty." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "claims is required and must be non-empty." } } diff --git a/tests/fixtures/parity/legacy/verify__batch_idempotency_conflict_409.json b/tests/fixtures/parity/legacy/verify__batch_idempotency_conflict_409.json index 0b70d8c..ece0599 100644 --- a/tests/fixtures/parity/legacy/verify__batch_idempotency_conflict_409.json +++ b/tests/fixtures/parity/legacy/verify__batch_idempotency_conflict_409.json @@ -1,12 +1,12 @@ { "status": 409, + "body": { + "detail": "A request with this Idempotency-Key is already in progress.", + "task_id": "7cff18d" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "A request with this Idempotency-Key is already in progress.", - "task_id": "7cff18d" } } diff --git a/tests/fixtures/parity/legacy/verify__batch_item_missing_claim_422.json b/tests/fixtures/parity/legacy/verify__batch_item_missing_claim_422.json index ad1fc01..aa586cc 100644 --- a/tests/fixtures/parity/legacy/verify__batch_item_missing_claim_422.json +++ b/tests/fixtures/parity/legacy/verify__batch_item_missing_claim_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -19,5 +14,10 @@ "msg": "Field required" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__batch_no_credits_402.json b/tests/fixtures/parity/legacy/verify__batch_no_credits_402.json index 5ec2c08..3bc3035 100644 --- a/tests/fixtures/parity/legacy/verify__batch_no_credits_402.json +++ b/tests/fixtures/parity/legacy/verify__batch_no_credits_402.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Insufficient credits for batch.", "code": "no_credits", @@ -13,8 +8,13 @@ "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "requested": 1, "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 10 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__batch_partial_202.json b/tests/fixtures/parity/legacy/verify__batch_partial_202.json index 5bfc144..8b689ea 100644 --- a/tests/fixtures/parity/legacy/verify__batch_partial_202.json +++ b/tests/fixtures/parity/legacy/verify__batch_partial_202.json @@ -1,18 +1,18 @@ { "status": 202, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "batch_id": "7ea9cdbd4064e3cf", "items": [ { - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "claim_text": "First claim." } ], "partial": true + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__batch_too_many_422.json b/tests/fixtures/parity/legacy/verify__batch_too_many_422.json index a865749..95e938a 100644 --- a/tests/fixtures/parity/legacy/verify__batch_too_many_422.json +++ b/tests/fixtures/parity/legacy/verify__batch_too_many_422.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Batch size exceeds maximum of 20." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Batch size exceeds maximum of 20." } } diff --git a/tests/fixtures/parity/legacy/verify__blank_claim_422.json b/tests/fixtures/parity/legacy/verify__blank_claim_422.json index 1ebb88e..367f620 100644 --- a/tests/fixtures/parity/legacy/verify__blank_claim_422.json +++ b/tests/fixtures/parity/legacy/verify__blank_claim_422.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Text is required." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Text is required." } } diff --git a/tests/fixtures/parity/legacy/verify__capacity_503.json b/tests/fixtures/parity/legacy/verify__capacity_503.json index 342c8aa..ea99c5b 100644 --- a/tests/fixtures/parity/legacy/verify__capacity_503.json +++ b/tests/fixtures/parity/legacy/verify__capacity_503.json @@ -1,15 +1,15 @@ { "status": 503, - "headers": { - "Content-Type": "application/json", - "Retry-After": "", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Lenz is at capacity right now — please resubmit after the stated wait. Nothing was charged.", "code": "capacity", "retry_after": "", "doc_url": "https://lenz.io/docs/errors#unavailable" + }, + "headers": { + "Content-Type": "application/json", + "Retry-After": "", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__delete_200.json b/tests/fixtures/parity/legacy/verify__delete_200.json new file mode 100644 index 0000000..cddadae --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__delete_200.json @@ -0,0 +1,11 @@ +{ + "status": 200, + "body": { + "ok": true + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/verify__delete_404.json b/tests/fixtures/parity/legacy/verify__delete_404.json new file mode 100644 index 0000000..538732c --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__delete_404.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "body": { + "detail": "Not found." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/verify__delete_not_yours_404.json b/tests/fixtures/parity/legacy/verify__delete_not_yours_404.json new file mode 100644 index 0000000..538732c --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__delete_not_yours_404.json @@ -0,0 +1,11 @@ +{ + "status": 404, + "body": { + "detail": "Not found." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/verify__idempotency_body_mismatch_422.json b/tests/fixtures/parity/legacy/verify__idempotency_body_mismatch_422.json index ef04562..2425649 100644 --- a/tests/fixtures/parity/legacy/verify__idempotency_body_mismatch_422.json +++ b/tests/fixtures/parity/legacy/verify__idempotency_body_mismatch_422.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Idempotency-Key reused with a different request body." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Idempotency-Key reused with a different request body." } } diff --git a/tests/fixtures/parity/legacy/verify__idempotency_conflict_409.json b/tests/fixtures/parity/legacy/verify__idempotency_conflict_409.json index 0eee4f9..7f1510e 100644 --- a/tests/fixtures/parity/legacy/verify__idempotency_conflict_409.json +++ b/tests/fixtures/parity/legacy/verify__idempotency_conflict_409.json @@ -1,12 +1,12 @@ { "status": 409, + "body": { + "detail": "A request with this Idempotency-Key is already in progress.", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "A request with this Idempotency-Key is already in progress.", - "task_id": "7cff18da2d979d6370e54a160be121ad" } } diff --git a/tests/fixtures/parity/legacy/verify__idempotency_key_replay.json b/tests/fixtures/parity/legacy/verify__idempotency_key_replay.json index b6dbf5e..1991e8a 100644 --- a/tests/fixtures/parity/legacy/verify__idempotency_key_replay.json +++ b/tests/fixtures/parity/legacy/verify__idempotency_key_replay.json @@ -1,13 +1,13 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "99a0a9ca89e1e72c" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued", - "chain_id": "78ac4e9f234ea3bd" } } diff --git a/tests/fixtures/parity/legacy/verify__implicit_dedup_200.json b/tests/fixtures/parity/legacy/verify__implicit_dedup_200.json index 011b4b9..79d39ad 100644 --- a/tests/fixtures/parity/legacy/verify__implicit_dedup_200.json +++ b/tests/fixtures/parity/legacy/verify__implicit_dedup_200.json @@ -1,12 +1,12 @@ { "status": 200, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued" } } diff --git a/tests/fixtures/parity/legacy/verify__implicit_repeat_replay.json b/tests/fixtures/parity/legacy/verify__implicit_repeat_replay.json index b6dbf5e..1991e8a 100644 --- a/tests/fixtures/parity/legacy/verify__implicit_repeat_replay.json +++ b/tests/fixtures/parity/legacy/verify__implicit_repeat_replay.json @@ -1,13 +1,13 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "99a0a9ca89e1e72c" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued", - "chain_id": "78ac4e9f234ea3bd" } } diff --git a/tests/fixtures/parity/legacy/verify__invalid_depth_422.json b/tests/fixtures/parity/legacy/verify__invalid_depth_422.json index 5c82768..6f04ccd 100644 --- a/tests/fixtures/parity/legacy/verify__invalid_depth_422.json +++ b/tests/fixtures/parity/legacy/verify__invalid_depth_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -20,5 +15,10 @@ } } ] + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__invalid_language_422.json b/tests/fixtures/parity/legacy/verify__invalid_language_422.json index 8127478..20922c8 100644 --- a/tests/fixtures/parity/legacy/verify__invalid_language_422.json +++ b/tests/fixtures/parity/legacy/verify__invalid_language_422.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unsupported language 'xx-not-a-language'. Supported: en, es, de, fr, it, pt, nl, sv, da, no, fi, bg. To ask for another language, contact us at https://lenz.io/contact." } } diff --git a/tests/fixtures/parity/legacy/verify__list_200.json b/tests/fixtures/parity/legacy/verify__list_200.json index c51ecc1..d7f28ec 100644 --- a/tests/fixtures/parity/legacy/verify__list_200.json +++ b/tests/fixtures/parity/legacy/verify__list_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -18,12 +13,12 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "language": "en" }, { - "verification_id": "7c83247c", + "verification_id": "4d44dfaf", "claim": "The Earth is round.", "domain": "Science", "entities": [], @@ -33,7 +28,7 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "modified_at": null, "language": "en" } @@ -41,5 +36,10 @@ "total": 2, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__list_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/verify__list_200_modified_at_crosses_midnight_by_minutes.json index bcbe8b3..e2419b1 100644 --- a/tests/fixtures/parity/legacy/verify__list_200_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/legacy/verify__list_200_modified_at_crosses_midnight_by_minutes.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -26,5 +21,10 @@ "total": 1, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__list_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/verify__list_200_modified_at_same_day_hours_apart.json index 9f2e07e..f5c69c0 100644 --- a/tests/fixtures/parity/legacy/verify__list_200_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/legacy/verify__list_200_modified_at_same_day_hours_apart.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -26,5 +21,10 @@ "total": 1, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__list_clamped_200.json b/tests/fixtures/parity/legacy/verify__list_clamped_200.json index 3d639d4..c208418 100644 --- a/tests/fixtures/parity/legacy/verify__list_clamped_200.json +++ b/tests/fixtures/parity/legacy/verify__list_clamped_200.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 0, "page": 1, "page_size": 100 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__list_empty_200.json b/tests/fixtures/parity/legacy/verify__list_empty_200.json index b419316..406be8e 100644 --- a/tests/fixtures/parity/legacy/verify__list_empty_200.json +++ b/tests/fixtures/parity/legacy/verify__list_empty_200.json @@ -1,14 +1,14 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [], "total": 0, "page": 1, "page_size": 20 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__list_page2_200.json b/tests/fixtures/parity/legacy/verify__list_page2_200.json index 38ebd2f..438afdc 100644 --- a/tests/fixtures/parity/legacy/verify__list_page2_200.json +++ b/tests/fixtures/parity/legacy/verify__list_page2_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "items": [ { @@ -18,7 +13,7 @@ "key_finding": "The Earth is approximately spherical in shape.", "executive_summary": "The claim is verified.", "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "language": "en" } @@ -26,5 +21,10 @@ "total": 3, "page": 2, "page_size": 2 + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__misnamed_field_422.json b/tests/fixtures/parity/legacy/verify__misnamed_field_422.json index 98527f4..c83cdc2 100644 --- a/tests/fixtures/parity/legacy/verify__misnamed_field_422.json +++ b/tests/fixtures/parity/legacy/verify__misnamed_field_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -22,5 +17,10 @@ "unrecognized_fields": [ "statement" ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__missing_claim_422.json b/tests/fixtures/parity/legacy/verify__missing_claim_422.json index f862968..591c22f 100644 --- a/tests/fixtures/parity/legacy/verify__missing_claim_422.json +++ b/tests/fixtures/parity/legacy/verify__missing_claim_422.json @@ -1,10 +1,5 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": [ { @@ -17,5 +12,10 @@ "msg": "Field required" } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__no_credits_402.json b/tests/fixtures/parity/legacy/verify__no_credits_402.json index 32f50ac..bf3a6b1 100644 --- a/tests/fixtures/parity/legacy/verify__no_credits_402.json +++ b/tests/fixtures/parity/legacy/verify__no_credits_402.json @@ -1,10 +1,5 @@ { "status": 402, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "No remaining claim checks.", "code": "no_credits", @@ -12,8 +7,13 @@ "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "remaining": 0, - "resets_at": "2026-10-01T09:14:00+00:00", + "resets_at": "2026-10-01T14:07:00+00:00", "credits_remaining": 0, "cost": 10 + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__replay_later_202.json b/tests/fixtures/parity/legacy/verify__replay_later_202.json index b6dbf5e..1991e8a 100644 --- a/tests/fixtures/parity/legacy/verify__replay_later_202.json +++ b/tests/fixtures/parity/legacy/verify__replay_later_202.json @@ -1,13 +1,13 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "99a0a9ca89e1e72c" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued", - "chain_id": "78ac4e9f234ea3bd" } } diff --git a/tests/fixtures/parity/legacy/verify__select_202.json b/tests/fixtures/parity/legacy/verify__select_202.json index 7229ce4..f35e0ac 100644 --- a/tests/fixtures/parity/legacy/verify__select_202.json +++ b/tests/fixtures/parity/legacy/verify__select_202.json @@ -1,21 +1,21 @@ { "status": 202, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "batch_id": "7ea9cdbd4064e3cf", "items": [ { - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "claim_text": "The Earth is round." }, { - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", "claim_text": "Water boils at 100C at sea level." } ] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__select_empty_422.json b/tests/fixtures/parity/legacy/verify__select_empty_422.json index e2c29b2..a688edc 100644 --- a/tests/fixtures/parity/legacy/verify__select_empty_422.json +++ b/tests/fixtures/parity/legacy/verify__select_empty_422.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "texts is required and must be non-empty." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "texts is required and must be non-empty." } } diff --git a/tests/fixtures/parity/legacy/verify__select_invalid_selection_422.json b/tests/fixtures/parity/legacy/verify__select_invalid_selection_422.json index 637ab1f..1102de9 100644 --- a/tests/fixtures/parity/legacy/verify__select_invalid_selection_422.json +++ b/tests/fixtures/parity/legacy/verify__select_invalid_selection_422.json @@ -1,13 +1,13 @@ { "status": 422, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "Selected text was not one of the offered claims: 'The Moon is made of cheese.'", "code": "invalid_selection", "error": "invalid_selection" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__select_no_selection_pending_409.json b/tests/fixtures/parity/legacy/verify__select_no_selection_pending_409.json index 3b60350..9d0e763 100644 --- a/tests/fixtures/parity/legacy/verify__select_no_selection_pending_409.json +++ b/tests/fixtures/parity/legacy/verify__select_no_selection_pending_409.json @@ -1,13 +1,13 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This task has no pending claim selection.", "code": "no_selection_pending", "error": "no_selection_pending" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__select_too_many_422.json b/tests/fixtures/parity/legacy/verify__select_too_many_422.json index 297aef4..8cab81a 100644 --- a/tests/fixtures/parity/legacy/verify__select_too_many_422.json +++ b/tests/fixtures/parity/legacy/verify__select_too_many_422.json @@ -1,11 +1,11 @@ { "status": 422, + "body": { + "detail": "Cannot select more than 20 claims." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Cannot select more than 20 claims." } } diff --git a/tests/fixtures/parity/legacy/verify__select_unknown_task_404.json b/tests/fixtures/parity/legacy/verify__select_unknown_task_404.json index 09c1e79..d005db5 100644 --- a/tests/fixtures/parity/legacy/verify__select_unknown_task_404.json +++ b/tests/fixtures/parity/legacy/verify__select_unknown_task_404.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Task not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Task not found." } } diff --git a/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json index 0350ab8..d175b8e 100644 --- a/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json +++ b/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -13,5 +8,10 @@ "failure_class": "cancelled", "retryable": false, "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_completed.json b/tests/fixtures/parity/legacy/verify__status_completed.json index 8e62516..2b34cd1 100644 --- a/tests/fixtures/parity/legacy/verify__status_completed.json +++ b/tests/fixtures/parity/legacy/verify__status_completed.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -30,7 +25,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_completed_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while.json index 8e62516..2b34cd1 100644 --- a/tests/fixtures/parity/legacy/verify__status_completed_after_a_while.json +++ b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -30,7 +25,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json index 3f7a898..068ec2d 100644 --- a/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json index 5a6b9fc..346ec77 100644 --- a/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/legacy/verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/verify__status_completed_modified_at_crosses_midnight_by_minutes.json index 3f7a898..068ec2d 100644 --- a/tests/fixtures/parity/legacy/verify__status_completed_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/legacy/verify__status_completed_modified_at_crosses_midnight_by_minutes.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/verify__status_completed_modified_at_same_day_hours_apart.json index 5a6b9fc..346ec77 100644 --- a/tests/fixtures/parity/legacy/verify__status_completed_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/legacy/verify__status_completed_modified_at_same_day_hours_apart.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_failed.json b/tests/fixtures/parity/legacy/verify__status_failed.json index 29f43b1..77f7bff 100644 --- a/tests/fixtures/parity/legacy/verify__status_failed.json +++ b/tests/fixtures/parity/legacy/verify__status_failed.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -13,5 +8,10 @@ "failure_class": "insufficient_evidence", "retryable": false, "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_failed_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_failed_after_a_while.json index f3a344f..a0010b0 100644 --- a/tests/fixtures/parity/legacy/verify__status_failed_after_a_while.json +++ b/tests/fixtures/parity/legacy/verify__status_failed_after_a_while.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -13,5 +8,10 @@ "failure_class": "internal", "retryable": false, "docs_url": "https://lenz.io/docs/errors#internal" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_failed_after_a_while_framing.json b/tests/fixtures/parity/legacy/verify__status_failed_after_a_while_framing.json index 2f2bf0c..a0c6b65 100644 --- a/tests/fixtures/parity/legacy/verify__status_failed_after_a_while_framing.json +++ b/tests/fixtures/parity/legacy/verify__status_failed_after_a_while_framing.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -13,5 +8,10 @@ "failure_class": "upstream_unavailable", "retryable": true, "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_failed_retryable.json b/tests/fixtures/parity/legacy/verify__status_failed_retryable.json index 3451f74..05f97a9 100644 --- a/tests/fixtures/parity/legacy/verify__status_failed_retryable.json +++ b/tests/fixtures/parity/legacy/verify__status_failed_retryable.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -13,5 +8,10 @@ "failure_class": "upstream_unavailable", "retryable": true, "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_needs_input.json b/tests/fixtures/parity/legacy/verify__status_needs_input.json index 463b486..4cb32ed 100644 --- a/tests/fixtures/parity/legacy/verify__status_needs_input.json +++ b/tests/fixtures/parity/legacy/verify__status_needs_input.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "needs_input", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -20,5 +15,10 @@ } ], "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_not_a_claim.json b/tests/fixtures/parity/legacy/verify__status_not_a_claim.json index f787d67..e900727 100644 --- a/tests/fixtures/parity/legacy/verify__status_not_a_claim.json +++ b/tests/fixtures/parity/legacy/verify__status_not_a_claim.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -14,5 +9,10 @@ "retryable": false, "docs_url": "https://lenz.io/docs/errors#invalid-input", "hint": "The input is a greeting, not a statement that can be checked. Send one factual claim, or run the text through /extract to enumerate its claims." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_not_a_claim_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_not_a_claim_after_a_while.json index ebe10ba..cd385ae 100644 --- a/tests/fixtures/parity/legacy/verify__status_not_a_claim_after_a_while.json +++ b/tests/fixtures/parity/legacy/verify__status_not_a_claim_after_a_while.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -13,5 +8,10 @@ "failure_class": "invalid_input", "retryable": false, "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_not_yours_404.json b/tests/fixtures/parity/legacy/verify__status_not_yours_404.json index 09c1e79..d005db5 100644 --- a/tests/fixtures/parity/legacy/verify__status_not_yours_404.json +++ b/tests/fixtures/parity/legacy/verify__status_not_yours_404.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Task not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Task not found." } } diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_completed.json b/tests/fixtures/parity/legacy/verify__status_older_run_completed.json index 8e62516..2b34cd1 100644 --- a/tests/fixtures/parity/legacy/verify__status_older_run_completed.json +++ b/tests/fixtures/parity/legacy/verify__status_older_run_completed.json @@ -1,15 +1,10 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "completed", "task_id": "7cff18da2d979d6370e54a160be121ad", "result": { - "verification_id": "4d44dfaf", + "verification_id": "68ca4aaf", "claim": "The Earth is round.", "visibility": "private", "depth": "standard", @@ -30,7 +25,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -95,5 +90,10 @@ "terms_version": "v1" } } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json b/tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json index 0a8ff9f..91a9dc7 100644 --- a/tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json +++ b/tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -13,5 +8,10 @@ "failure_class": "internal", "retryable": false, "docs_url": "https://lenz.io/docs/errors#internal" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json b/tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json index 29f43b1..77f7bff 100644 --- a/tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json +++ b/tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -13,5 +8,10 @@ "failure_class": "insufficient_evidence", "retryable": false, "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json b/tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json index 8ff9472..5e5d47b 100644 --- a/tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json +++ b/tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -12,8 +7,13 @@ "step": "research", "index": 2, "total": 5, - "elapsed_seconds": 2, + "elapsed_seconds": 1, "poll_after_seconds": 5 } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_processing.json b/tests/fixtures/parity/legacy/verify__status_processing.json index 8ff9472..5e5d47b 100644 --- a/tests/fixtures/parity/legacy/verify__status_processing.json +++ b/tests/fixtures/parity/legacy/verify__status_processing.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -12,8 +7,13 @@ "step": "research", "index": 2, "total": 5, - "elapsed_seconds": 2, + "elapsed_seconds": 1, "poll_after_seconds": 5 } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_processing_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_processing_after_a_while.json index 077dee8..e0638f1 100644 --- a/tests/fixtures/parity/legacy/verify__status_processing_after_a_while.json +++ b/tests/fixtures/parity/legacy/verify__status_processing_after_a_while.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -12,8 +7,13 @@ "step": "adjudication", "index": 4, "total": 5, - "elapsed_seconds": 2, + "elapsed_seconds": 1, "poll_after_seconds": 5 } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_task_stuck.json b/tests/fixtures/parity/legacy/verify__status_task_stuck.json index 2504c91..383a365 100644 --- a/tests/fixtures/parity/legacy/verify__status_task_stuck.json +++ b/tests/fixtures/parity/legacy/verify__status_task_stuck.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "status": "failed", "task_id": "7cff18da2d979d6370e54a160be121ad", @@ -13,5 +8,10 @@ "failure_class": "upstream_unavailable", "retryable": true, "docs_url": "https://lenz.io/docs/errors#upstream-unavailable" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__status_unknown_404.json b/tests/fixtures/parity/legacy/verify__status_unknown_404.json index 09c1e79..d005db5 100644 --- a/tests/fixtures/parity/legacy/verify__status_unknown_404.json +++ b/tests/fixtures/parity/legacy/verify__status_unknown_404.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Task not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Task not found." } } diff --git a/tests/fixtures/parity/legacy/verify__submit_202.json b/tests/fixtures/parity/legacy/verify__submit_202.json index b6dbf5e..1991e8a 100644 --- a/tests/fixtures/parity/legacy/verify__submit_202.json +++ b/tests/fixtures/parity/legacy/verify__submit_202.json @@ -1,13 +1,13 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "99a0a9ca89e1e72c" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued", - "chain_id": "78ac4e9f234ea3bd" } } diff --git a/tests/fixtures/parity/legacy/verify__submit_202_options.json b/tests/fixtures/parity/legacy/verify__submit_202_options.json index b6dbf5e..1991e8a 100644 --- a/tests/fixtures/parity/legacy/verify__submit_202_options.json +++ b/tests/fixtures/parity/legacy/verify__submit_202_options.json @@ -1,13 +1,13 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "99a0a9ca89e1e72c" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued", - "chain_id": "78ac4e9f234ea3bd" } } diff --git a/tests/fixtures/parity/legacy/verify__submit_202_text_alias.json b/tests/fixtures/parity/legacy/verify__submit_202_text_alias.json index b6dbf5e..1991e8a 100644 --- a/tests/fixtures/parity/legacy/verify__submit_202_text_alias.json +++ b/tests/fixtures/parity/legacy/verify__submit_202_text_alias.json @@ -1,13 +1,13 @@ { "status": 202, + "body": { + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "queued", + "chain_id": "99a0a9ca89e1e72c" + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued", - "chain_id": "78ac4e9f234ea3bd" } } diff --git a/tests/fixtures/parity/legacy/verify__unauthenticated_401.json b/tests/fixtures/parity/legacy/verify__unauthenticated_401.json index 8dd2fb4..54ec0b6 100644 --- a/tests/fixtures/parity/legacy/verify__unauthenticated_401.json +++ b/tests/fixtures/parity/legacy/verify__unauthenticated_401.json @@ -1,11 +1,11 @@ { "status": 401, + "body": { + "detail": "Unauthorized" + }, "headers": { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Unauthorized" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_200.json b/tests/fixtures/parity/legacy/verify__verification_200.json index b4a8eae..50599ee 100644 --- a/tests/fixtures/parity/legacy/verify__verification_200.json +++ b/tests/fixtures/parity/legacy/verify__verification_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,7 +22,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_200_covered.json b/tests/fixtures/parity/legacy/verify__verification_200_covered.json index 5ff9574..fa6aef7 100644 --- a/tests/fixtures/parity/legacy/verify__verification_200_covered.json +++ b/tests/fixtures/parity/legacy/verify__verification_200_covered.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,7 +22,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -81,13 +76,18 @@ "coverage": { "status": "covered", "reasons": [], - "certificate_id": "def7232d27eda03cf261569aee9948c5", - "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", "as_of": "2026-03-10T12:01:30+00:00", "currency": "EUR", "cap": 10000, "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_crosses_midnight_by_minutes.json index c57efcb..2376a58 100644 --- a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_crosses_midnight_by_minutes.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_null.json b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_null.json index b4a8eae..50599ee 100644 --- a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_null.json +++ b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_null.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,7 +22,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_same_day_hours_apart.json index 4480b71..47e455c 100644 --- a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_same_day_hours_apart.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_set.json b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_set.json index c57efcb..2376a58 100644 --- a/tests/fixtures/parity/legacy/verify__verification_200_modified_at_set.json +++ b/tests/fixtures/parity/legacy/verify__verification_200_modified_at_set.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_200_result_json_conclusion_only.json b/tests/fixtures/parity/legacy/verify__verification_200_result_json_conclusion_only.json index 86594fb..b5cdcd6 100644 --- a/tests/fixtures/parity/legacy/verify__verification_200_result_json_conclusion_only.json +++ b/tests/fixtures/parity/legacy/verify__verification_200_result_json_conclusion_only.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -20,7 +15,7 @@ "executive_summary": "The claim is verified.", "warnings": [], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [], "audit": { @@ -52,5 +47,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_200_result_json_empty.json b/tests/fixtures/parity/legacy/verify__verification_200_result_json_empty.json index 86594fb..b5cdcd6 100644 --- a/tests/fixtures/parity/legacy/verify__verification_200_result_json_empty.json +++ b/tests/fixtures/parity/legacy/verify__verification_200_result_json_empty.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -20,7 +15,7 @@ "executive_summary": "The claim is verified.", "warnings": [], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [], "audit": { @@ -52,5 +47,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_200_result_json_old_shape_strings.json b/tests/fixtures/parity/legacy/verify__verification_200_result_json_old_shape_strings.json index 34ae356..3250756 100644 --- a/tests/fixtures/parity/legacy/verify__verification_200_result_json_old_shape_strings.json +++ b/tests/fixtures/parity/legacy/verify__verification_200_result_json_old_shape_strings.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -20,7 +15,7 @@ "executive_summary": "The claim is verified.", "warnings": [], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -68,5 +63,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_404.json b/tests/fixtures/parity/legacy/verify__verification_404.json index 3bf3204..538732c 100644 --- a/tests/fixtures/parity/legacy/verify__verification_404.json +++ b/tests/fixtures/parity/legacy/verify__verification_404.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Not found." } } diff --git a/tests/fixtures/parity/legacy/verify__verification_by_task_id_200.json b/tests/fixtures/parity/legacy/verify__verification_by_task_id_200.json index b4a8eae..50599ee 100644 --- a/tests/fixtures/parity/legacy/verify__verification_by_task_id_200.json +++ b/tests/fixtures/parity/legacy/verify__verification_by_task_id_200.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,7 +22,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_failed_409.json b/tests/fixtures/parity/legacy/verify__verification_failed_409.json index fc6de0a..fdb3f4f 100644 --- a/tests/fixtures/parity/legacy/verify__verification_failed_409.json +++ b/tests/fixtures/parity/legacy/verify__verification_failed_409.json @@ -1,10 +1,5 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This check failed and has no result.", "code": "verification_failed", @@ -15,5 +10,10 @@ "failure_class": "insufficient_evidence", "retryable": false, "docs_url": "https://lenz.io/docs/errors#insufficient-evidence" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_failed_409_not_a_claim_after_a_while.json b/tests/fixtures/parity/legacy/verify__verification_failed_409_not_a_claim_after_a_while.json index d0c0aa6..41c53e8 100644 --- a/tests/fixtures/parity/legacy/verify__verification_failed_409_not_a_claim_after_a_while.json +++ b/tests/fixtures/parity/legacy/verify__verification_failed_409_not_a_claim_after_a_while.json @@ -1,10 +1,5 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This check failed and has no result.", "code": "verification_failed", @@ -15,5 +10,10 @@ "failure_class": "invalid_input", "retryable": false, "docs_url": "https://lenz.io/docs/errors#invalid-input" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_not_ready_409.json b/tests/fixtures/parity/legacy/verify__verification_not_ready_409.json index d5ace3f..dae092f 100644 --- a/tests/fixtures/parity/legacy/verify__verification_not_ready_409.json +++ b/tests/fixtures/parity/legacy/verify__verification_not_ready_409.json @@ -1,15 +1,15 @@ { "status": 409, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This check is still running.", "code": "verification_not_ready", "status": "processing", "task_id": "7cff18da2d979d6370e54a160be121ad", "hint": "Poll GET /verify/status/7cff18da2d979d6370e54a160be121ad until it completes, then read its result." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_not_yours_404.json b/tests/fixtures/parity/legacy/verify__verification_not_yours_404.json index 3bf3204..538732c 100644 --- a/tests/fixtures/parity/legacy/verify__verification_not_yours_404.json +++ b/tests/fixtures/parity/legacy/verify__verification_not_yours_404.json @@ -1,11 +1,11 @@ { "status": 404, + "body": { + "detail": "Not found." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Not found." } } diff --git a/tests/fixtures/parity/legacy/verify__verification_public_anonymous.json b/tests/fixtures/parity/legacy/verify__verification_public_anonymous.json index 6f459a1..2500a41 100644 --- a/tests/fixtures/parity/legacy/verify__verification_public_anonymous.json +++ b/tests/fixtures/parity/legacy/verify__verification_public_anonymous.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,7 +22,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -79,5 +74,10 @@ }, "language": "en", "coverage": null + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_public_of_another_user.json b/tests/fixtures/parity/legacy/verify__verification_public_of_another_user.json index b81a9a7..1bd991f 100644 --- a/tests/fixtures/parity/legacy/verify__verification_public_of_another_user.json +++ b/tests/fixtures/parity/legacy/verify__verification_public_of_another_user.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,7 +22,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_unknown_task_404.json b/tests/fixtures/parity/legacy/verify__verification_unknown_task_404.json index cfc00c7..379c918 100644 --- a/tests/fixtures/parity/legacy/verify__verification_unknown_task_404.json +++ b/tests/fixtures/parity/legacy/verify__verification_unknown_task_404.json @@ -1,12 +1,12 @@ { "status": 404, + "body": { + "detail": "Not found.", + "hint": "Unknown id. If this is a task_id, poll GET /verify/status/dddddddddddddddddddddddddddddddd for its status." + }, "headers": { "Content-Type": "application/json", "Cache-Control": "no-store", "Vary": "origin" - }, - "body": { - "detail": "Not found.", - "hint": "Unknown id. If this is a task_id, poll GET /verify/status/dddddddddddddddddddddddddddddddd for its status." } } diff --git a/tests/fixtures/parity/legacy/verify__verification_zero_retention_expired_410.json b/tests/fixtures/parity/legacy/verify__verification_zero_retention_expired_410.json index 010b258..924d468 100644 --- a/tests/fixtures/parity/legacy/verify__verification_zero_retention_expired_410.json +++ b/tests/fixtures/parity/legacy/verify__verification_zero_retention_expired_410.json @@ -1,13 +1,13 @@ { "status": 410, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "detail": "This verification is no longer available: its account removes verifications after a set period.", "code": "purged", "purged_at": "2026-10-01T09:07:00.123456+00:00" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__verification_zero_retention_served.json b/tests/fixtures/parity/legacy/verify__verification_zero_retention_served.json index 21c8d25..89895ad 100644 --- a/tests/fixtures/parity/legacy/verify__verification_zero_retention_served.json +++ b/tests/fixtures/parity/legacy/verify__verification_zero_retention_served.json @@ -1,10 +1,5 @@ { "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - }, "body": { "verification_id": "68ca4aaf", "claim": "The Earth is round.", @@ -27,7 +22,7 @@ "Relies on limited sources" ], "suggested_rewrite": null, - "created_at": "2026-10-01T09:14:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -91,5 +86,10 @@ "aggregate": 500000, "terms_version": "v1" } + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" } } diff --git a/tests/fixtures/parity/legacy/verify__webhook_secret_missing_422.json b/tests/fixtures/parity/legacy/verify__webhook_secret_missing_422.json new file mode 100644 index 0000000..bae889d --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__webhook_secret_missing_422.json @@ -0,0 +1,12 @@ +{ + "status": 422, + "body": { + "detail": "webhook_url was supplied but this API key has no HMAC secret. Generate one at https://lenz.io/api-credentials (Webhooks panel) before submitting webhook-delivered calls.", + "code": "webhook_secret_missing" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__certificate_timestamped.json b/tests/fixtures/parity/legacy/webhook__certificate_timestamped.json index a47b14c..eaeb5fb 100644 --- a/tests/fixtures/parity/legacy/webhook__certificate_timestamped.json +++ b/tests/fixtures/parity/legacy/webhook__certificate_timestamped.json @@ -1,10 +1,9 @@ { "status": 200, - "headers": {}, "body": { "event": "certificate.timestamped", "verification_id": "68ca4aaf", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "batch_id": null, "status": "completed", "result": null, @@ -12,8 +11,8 @@ "coverage": { "status": "covered", "reasons": [], - "certificate_id": "def7232d27eda03cf261569aee9948c5", - "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", "as_of": "2026-09-03T10:00:00+00:00", "currency": "EUR", "cap": 10000, @@ -22,6 +21,7 @@ }, "error": null, "attempt": 1, - "delivered_at": "2026-10-01T09:28:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__certificate_timestamped_review_child.json b/tests/fixtures/parity/legacy/webhook__certificate_timestamped_review_child.json index 028efa3..6c67b46 100644 --- a/tests/fixtures/parity/legacy/webhook__certificate_timestamped_review_child.json +++ b/tests/fixtures/parity/legacy/webhook__certificate_timestamped_review_child.json @@ -1,10 +1,9 @@ { "status": 200, - "headers": {}, "body": { "event": "certificate.timestamped", "verification_id": "68ca4aaf", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "batch_id": null, "status": "completed", "result": null, @@ -12,8 +11,8 @@ "coverage": { "status": "covered", "reasons": [], - "certificate_id": "def7232d27eda03cf261569aee9948c5", - "certificate_url": "https://lenz.io/certificate/def7232d27eda03cf261569aee9948c5", + "certificate_id": "95b0f3dd008b46846b6dd6b15099e2b3", + "certificate_url": "https://lenz.io/certificate/95b0f3dd008b46846b6dd6b15099e2b3", "as_of": "2026-09-03T10:00:00+00:00", "currency": "EUR", "cap": 10000, @@ -23,6 +22,7 @@ "error": null, "review_id": "d6b2bd72", "attempt": 1, - "delivered_at": "2026-10-01T09:28:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__citecheck_completed.json b/tests/fixtures/parity/legacy/webhook__citecheck_completed.json index 447a098..b8a79a6 100644 --- a/tests/fixtures/parity/legacy/webhook__citecheck_completed.json +++ b/tests/fixtures/parity/legacy/webhook__citecheck_completed.json @@ -1,18 +1,17 @@ { "status": 200, - "headers": {}, "body": { "event": "citecheck.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "citecheck_id": "98c0e7d5", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "citecheck_id": "8eb49302", + "task_id": "7cff18da2d979d6370e54a160be121ad", "status": "completed", "citecheck": { - "citecheck_id": "98c0e7d5", + "citecheck_id": "8eb49302", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -129,6 +128,7 @@ "more_citations": [] }, "attempt": 1, - "delivered_at": "2026-10-01T09:42:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__citecheck_failed.json b/tests/fixtures/parity/legacy/webhook__citecheck_failed.json index 19627f2..7738080 100644 --- a/tests/fixtures/parity/legacy/webhook__citecheck_failed.json +++ b/tests/fixtures/parity/legacy/webhook__citecheck_failed.json @@ -1,18 +1,17 @@ { "status": 200, - "headers": {}, "body": { "event": "citecheck.failed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "citecheck_id": "98c0e7d5", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "citecheck_id": "8eb49302", + "task_id": "7cff18da2d979d6370e54a160be121ad", "status": "failed", "citecheck": { - "citecheck_id": "98c0e7d5", + "citecheck_id": "8eb49302", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "poll_after_seconds": null, "policy": { @@ -42,6 +41,7 @@ "more_citations": null }, "attempt": 1, - "delivered_at": "2026-10-01T09:42:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__review_completed.json b/tests/fixtures/parity/legacy/webhook__review_completed.json index 1149eea..ea72c1c 100644 --- a/tests/fixtures/parity/legacy/webhook__review_completed.json +++ b/tests/fixtures/parity/legacy/webhook__review_completed.json @@ -1,19 +1,18 @@ { "status": 200, - "headers": {}, "body": { "event": "review.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "review_id": "8cb2f001", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "review_id": "d6b2bd72", + "task_id": "7cff18da2d979d6370e54a160be121ad", "status": "completed", "review": { - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "issues_found", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -65,10 +64,10 @@ "verdict": "False", "confidence": "high", "source": "verification", - "verification_id": "8ceed6f0", + "verification_id": "68ca4aaf", "verification_status": "completed", - "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", - "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "escalation": { "matched_rules": [ "verdict" @@ -118,8 +117,8 @@ "verification": { "status": "completed", "content_status": "available", - "task_id": "5ccac77f53ff49328ec134fb29d66542", - "verification_id": "8ceed6f0", + "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "verification_id": "68ca4aaf", "claim": "The Eiffel Tower is in Berlin.", "language": "en", "visibility": "private", @@ -135,10 +134,10 @@ "warnings": [ "w1" ], - "created_at": "2026-10-01T09:56:00.123456+00:00", + "created_at": "2026-10-01T09:14:00.123456+00:00", "modified_at": null, - "verification_url": "https://lenz.io/api/v1/verifications/8ceed6f0", - "url": "https://lenz.io/c/the-earth-is-round-8ceed6f0", + "verification_url": "https://lenz.io/api/v1/verifications/68ca4aaf", + "url": "https://lenz.io/c/the-earth-is-round-68ca4aaf", "failure": null }, "suggested_edits": null @@ -151,6 +150,7 @@ "more_citations": [] }, "attempt": 1, - "delivered_at": "2026-10-01T10:03:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__review_completed_key_default_url.json b/tests/fixtures/parity/legacy/webhook__review_completed_key_default_url.json index 35c4a30..7c98f76 100644 --- a/tests/fixtures/parity/legacy/webhook__review_completed_key_default_url.json +++ b/tests/fixtures/parity/legacy/webhook__review_completed_key_default_url.json @@ -1,19 +1,18 @@ { "status": 200, - "headers": {}, "body": { "event": "review.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "review_id": "8cb2f001", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "review_id": "d6b2bd72", + "task_id": "7cff18da2d979d6370e54a160be121ad", "status": "completed", "review": { - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "view": "full", "status": "completed", "outcome": "clean", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -100,6 +99,7 @@ "more_citations": [] }, "attempt": 1, - "delivered_at": "2026-10-01T09:42:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__review_completed_oversized_rebuilt.json b/tests/fixtures/parity/legacy/webhook__review_completed_oversized_rebuilt.json index 6248eaf..cd09384 100644 --- a/tests/fixtures/parity/legacy/webhook__review_completed_oversized_rebuilt.json +++ b/tests/fixtures/parity/legacy/webhook__review_completed_oversized_rebuilt.json @@ -1,11 +1,11 @@ { "status": 200, - "headers": {}, "body": { "event": "review.completed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "review_id": "8cb2f001", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "review_id": "d6b2bd72", + "task_id": "7cff18da2d979d6370e54a160be121ad", "status": "completed" - } + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__review_failed.json b/tests/fixtures/parity/legacy/webhook__review_failed.json index 294d967..364b930 100644 --- a/tests/fixtures/parity/legacy/webhook__review_failed.json +++ b/tests/fixtures/parity/legacy/webhook__review_failed.json @@ -1,19 +1,18 @@ { "status": 200, - "headers": {}, "body": { "event": "review.failed", "event_id": "evt_09301870aba4e8f6ae33ff60", - "review_id": "8cb2f001", - "task_id": "13610f0b64b63e006dc66364e0e82c85", + "review_id": "d6b2bd72", + "task_id": "7cff18da2d979d6370e54a160be121ad", "status": "failed", "review": { - "review_id": "8cb2f001", + "review_id": "d6b2bd72", "view": "full", "status": "failed", "outcome": "unchecked", - "created_at": "2026-10-01T09:28:00.123456Z", - "completed_at": "2026-10-01T09:35:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", "language": "en", "policy": { "verdicts": [ @@ -68,6 +67,7 @@ "more_citations": null }, "attempt": 1, - "delivered_at": "2026-10-01T09:42:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__test_fire.json b/tests/fixtures/parity/legacy/webhook__test_fire.json new file mode 100644 index 0000000..131b2f2 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__test_fire.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "body": { + "attempt": 1, + "batch_id": null, + "coverage": null, + "delivered_at": "2026-10-01T11:07:00.123456+00:00", + "error": null, + "event": "webhook.test", + "needs_input": null, + "result": null, + "status": "test", + "task_id": "test_7cff18da2d979d63", + "verification_id": null + }, + "headers": {} +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_completed.json b/tests/fixtures/parity/legacy/webhook__verification_completed.json index 68b9dbb..04dae69 100644 --- a/tests/fixtures/parity/legacy/webhook__verification_completed.json +++ b/tests/fixtures/parity/legacy/webhook__verification_completed.json @@ -1,10 +1,9 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.completed", "verification_id": "68ca4aaf", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "batch_id": null, "status": "completed", "result": { @@ -33,7 +32,7 @@ "Only two sources were found." ], "suggested_rewrite": "The Eiffel Tower is in Paris.", - "created_at": "2026-10-01T09:21:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -102,6 +101,7 @@ "coverage": null, "error": null, "attempt": 1, - "delivered_at": "2026-10-01T09:28:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__verification_completed_in_batch.json b/tests/fixtures/parity/legacy/webhook__verification_completed_in_batch.json index be55b0a..efa7cc2 100644 --- a/tests/fixtures/parity/legacy/webhook__verification_completed_in_batch.json +++ b/tests/fixtures/parity/legacy/webhook__verification_completed_in_batch.json @@ -1,11 +1,10 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.completed", "verification_id": "68ca4aaf", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", - "batch_id": "4ee0c3aa1b671eaa", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "batch_id": "7ea9cdbd4064e3cf", "status": "completed", "result": { "verification_id": "68ca4aaf", @@ -33,7 +32,7 @@ "Only two sources were found." ], "suggested_rewrite": "The Eiffel Tower is in Paris.", - "created_at": "2026-10-01T09:28:00.123456+00:00", + "created_at": "2026-10-01T09:07:00.123456+00:00", "modified_at": null, "sources": [ { @@ -102,6 +101,7 @@ "coverage": null, "error": null, "attempt": 1, - "delivered_at": "2026-10-01T09:35:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json b/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json index c9af648..558176e 100644 --- a/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json +++ b/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_crosses_midnight_by_minutes.json @@ -1,10 +1,9 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.completed", "verification_id": "68ca4aaf", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "batch_id": null, "status": "completed", "result": { @@ -102,6 +101,7 @@ "coverage": null, "error": null, "attempt": 1, - "delivered_at": "2026-10-01T09:35:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_same_day_hours_apart.json b/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_same_day_hours_apart.json index 6d2f10a..dee4598 100644 --- a/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_same_day_hours_apart.json +++ b/tests/fixtures/parity/legacy/webhook__verification_completed_modified_at_same_day_hours_apart.json @@ -1,10 +1,9 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.completed", "verification_id": "68ca4aaf", - "task_id": "0b92b443eb69b9c71434fdc64f2ca8be", + "task_id": "7cff18da2d979d6370e54a160be121ad", "batch_id": null, "status": "completed", "result": { @@ -102,6 +101,7 @@ "coverage": null, "error": null, "attempt": 1, - "delivered_at": "2026-10-01T09:28:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__verification_completed_sparse_result.json b/tests/fixtures/parity/legacy/webhook__verification_completed_sparse_result.json new file mode 100644 index 0000000..643b479 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_completed_sparse_result.json @@ -0,0 +1,52 @@ +{ + "status": 200, + "body": { + "event": "verification.completed", + "verification_id": "68ca4aaf", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "batch_id": null, + "status": "completed", + "result": { + "verification_id": "68ca4aaf", + "claim": "The Eiffel Tower is in Berlin.", + "visibility": "private", + "depth": "standard", + "domain": "", + "entities": [], + "presumed_intent": "", + "verdict": "", + "confidence": "low", + "lenz_score": null, + "key_finding": "", + "executive_summary": "", + "warnings": [], + "suggested_rewrite": null, + "created_at": "", + "modified_at": null, + "sources": [], + "audit": { + "adjudication_summary": "", + "assessments": [], + "debate_pro": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "debate_con": { + "role": "", + "argument": "", + "rebuttal": "" + }, + "panel_agreement": "" + }, + "language": "en", + "coverage": null + }, + "needs_input": null, + "coverage": null, + "error": null, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_failed_error_none.json b/tests/fixtures/parity/legacy/webhook__verification_failed_error_none.json new file mode 100644 index 0000000..6647535 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_failed_error_none.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "body": { + "event": "verification.failed", + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "batch_id": null, + "status": "failed", + "result": null, + "needs_input": null, + "coverage": null, + "error": null, + "failure_class": "internal", + "retryable": false, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_failed_insufficient_evidence.json b/tests/fixtures/parity/legacy/webhook__verification_failed_insufficient_evidence.json index e44f526..ecfcdbd 100644 --- a/tests/fixtures/parity/legacy/webhook__verification_failed_insufficient_evidence.json +++ b/tests/fixtures/parity/legacy/webhook__verification_failed_insufficient_evidence.json @@ -1,6 +1,5 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.failed", "verification_id": null, @@ -14,6 +13,7 @@ "failure_class": "insufficient_evidence", "retryable": false, "attempt": 1, - "delivered_at": "2026-10-01T09:14:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__verification_failed_not_a_claim.json b/tests/fixtures/parity/legacy/webhook__verification_failed_not_a_claim.json index d27f1f1..5dc356d 100644 --- a/tests/fixtures/parity/legacy/webhook__verification_failed_not_a_claim.json +++ b/tests/fixtures/parity/legacy/webhook__verification_failed_not_a_claim.json @@ -1,6 +1,5 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.failed", "verification_id": null, @@ -14,6 +13,7 @@ "failure_class": "invalid_input", "retryable": false, "attempt": 1, - "delivered_at": "2026-10-01T09:14:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__verification_failed_upstream_unavailable.json b/tests/fixtures/parity/legacy/webhook__verification_failed_upstream_unavailable.json index 14ba36a..867b62b 100644 --- a/tests/fixtures/parity/legacy/webhook__verification_failed_upstream_unavailable.json +++ b/tests/fixtures/parity/legacy/webhook__verification_failed_upstream_unavailable.json @@ -1,6 +1,5 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.failed", "verification_id": null, @@ -14,6 +13,7 @@ "failure_class": "upstream_unavailable", "retryable": true, "attempt": 1, - "delivered_at": "2026-10-01T09:14:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/legacy/webhook__verification_needs_input_hint_none.json b/tests/fixtures/parity/legacy/webhook__verification_needs_input_hint_none.json new file mode 100644 index 0000000..0a349f3 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_needs_input_hint_none.json @@ -0,0 +1,27 @@ +{ + "status": 200, + "body": { + "event": "verification.needs_input", + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "batch_id": null, + "status": "needs_input", + "result": null, + "needs_input": { + "reason": "multi_claim", + "claims": [ + { + "text": "The Eiffel Tower is in Berlin.", + "domain": "" + } + ], + "hint": null + }, + "error": null, + "coverage": null, + "review_id": null, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_needs_input_multi_claim.json b/tests/fixtures/parity/legacy/webhook__verification_needs_input_multi_claim.json index 4d08431..9395c84 100644 --- a/tests/fixtures/parity/legacy/webhook__verification_needs_input_multi_claim.json +++ b/tests/fixtures/parity/legacy/webhook__verification_needs_input_multi_claim.json @@ -1,6 +1,5 @@ { "status": 200, - "headers": {}, "body": { "event": "verification.needs_input", "verification_id": null, @@ -25,6 +24,7 @@ "coverage": null, "error": null, "attempt": 1, - "delivered_at": "2026-10-01T09:14:00.123456+00:00" - } + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} } diff --git a/tests/fixtures/parity/pickles.json b/tests/fixtures/parity/pickles.json new file mode 100644 index 0000000..cf1551e --- /dev/null +++ b/tests/fixtures/parity/pickles.json @@ -0,0 +1,156 @@ +{ + "account__library_empty.json": "gASVuQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwLTGlicmFyeUxpc3SUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwFaXRlbXOUXZSMBXRvdGFslEsAjARwYWdllEsBjAlwYWdlX3NpemWUSxR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgKaAtoCWgHkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViLg==", + "account__library_item_modified_at_crosses_midnight_by_minutes.json": "gASVdwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwLTGlicmFyeUxpc3SUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwFaXRlbXOUXZRoAIwLTGlicmFyeUl0ZW2Uk5QpgZR9lChoBX2UKIwPdmVyaWZpY2F0aW9uX2lklIwINjhjYTRhYWaUjAVjbGFpbZSMEEEgbGlicmFyeSBjbGFpbS6UjAZkb21haW6UjACUjAhlbnRpdGllc5RdlIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwDbG93lIwKbGVuel9zY29yZZRLCowLa2V5X2ZpbmRpbmeUjC5UaGUgRWFydGggaXMgYXBwcm94aW1hdGVseSBzcGhlcmljYWwgaW4gc2hhcGUulIwRZXhlY3V0aXZlX3N1bW1hcnmUjBZUaGUgY2xhaW0gaXMgdmVyaWZpZWQulIwRc3VnZ2VzdGVkX3Jld3JpdGWUTowKY3JlYXRlZF9hdJSMIDIwMjYtMTAtMDFUMjM6NTU6MDAuMTIzNDU2KzAwOjAwlIwLbW9kaWZpZWRfYXSUjCAyMDI2LTEwLTAyVDAwOjAzOjAwLjEyMzQ1NiswMDowMJSMCGxhbmd1YWdllIwCZW6UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoEGgOaBRoGGggaBJoFmgkaCJoHWgaaBtoH5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBXRvdGFslEsBjARwYWdllEsBjAlwYWdlX3NpemWUSxR1aCZ9lGgoj5QoaCxoLWgraAeQaCpOdWIu", + "account__library_item_modified_at_same_day_hours_apart.json": "gASVVQIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwLTGlicmFyeUxpc3SUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwFaXRlbXOUXZRoAIwLTGlicmFyeUl0ZW2Uk5QpgZR9lChoBX2UKIwPdmVyaWZpY2F0aW9uX2lklIwINjhjYTRhYWaUjAVjbGFpbZSMEEEgbGlicmFyeSBjbGFpbS6UjAZkb21haW6UjACUjAhlbnRpdGllc5RdlIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwDbG93lIwKbGVuel9zY29yZZRLCowLa2V5X2ZpbmRpbmeUjC5UaGUgRWFydGggaXMgYXBwcm94aW1hdGVseSBzcGhlcmljYWwgaW4gc2hhcGUulIwRZXhlY3V0aXZlX3N1bW1hcnmUjBZUaGUgY2xhaW0gaXMgdmVyaWZpZWQulIwRc3VnZ2VzdGVkX3Jld3JpdGWUTowKY3JlYXRlZF9hdJSMIDIwMjYtMTAtMDFUMDg6MDA6MDAuMTIzNDU2KzAwOjAwlIwLbW9kaWZpZWRfYXSUTowIbGFuZ3VhZ2WUjAJlbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgQaA5oFGgYaCBoEmgWaCNoImgdaBpoG2gfkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViYYwFdG90YWyUSwGMBHBhZ2WUSwGMCXBhZ2Vfc2l6ZZRLFHVoJX2UaCePlChoK2gsaCpoB5BoKU51Yi4=", + "account__library_items.json": "gASVWQQAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwLTGlicmFyeUxpc3SUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwFaXRlbXOUXZQoaACMC0xpYnJhcnlJdGVtlJOUKYGUfZQoaAV9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBBMaWJyYXJ5IGNsYWltIDAulIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjANsb3eUjApsZW56X3Njb3JllEsKjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjBFzdWdnZXN0ZWRfcmV3cml0ZZROjApjcmVhdGVkX2F0lIwgMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTYrMDA6MDCUjAttb2RpZmllZF9hdJROjAhsYW5ndWFnZZSMAmVulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBBoDmgUaBhoIGgSaBZoI2giaB1oGmgbaB+QjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoCimBlH2UKGgFfZQoaA6MCDRkNDRkZmFmlGgQjBBMaWJyYXJ5IGNsYWltIDEulGgSjAdTY2llbmNllGgUXZRoFowEVHJ1ZZRoGIwDbG93lGgaSwpoG4wuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpRoHYwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpRoH05oIIwgMjAyNi0xMC0wMVQwOToxNDowMC4xMjM0NTYrMDA6MDCUaCJOaCOMAmVulHVoJX2UaCePlChoEGgOaBRoGGggaBJoFmgjaCJoHWgaaBtoH5BoKU51YmgKKYGUfZQoaAV9lChoDowIN2M4MzI0N2OUaBCMEExpYnJhcnkgY2xhaW0gMi6UaBKMB1NjaWVuY2WUaBRdlGgWjARUcnVllGgYjANsb3eUaBpLCmgbjC5UaGUgRWFydGggaXMgYXBwcm94aW1hdGVseSBzcGhlcmljYWwgaW4gc2hhcGUulGgdjBZUaGUgY2xhaW0gaXMgdmVyaWZpZWQulGgfTmggjCAyMDI2LTEwLTAxVDA5OjIxOjAwLjEyMzQ1NiswMDowMJRoIk5oI4wCZW6UdWglfZRoJ4+UKGgQaA5oFGgYaCBoEmgWaCNoImgdaBpoG2gfkGgpTnViZYwFdG90YWyUSwOMBHBhZ2WUSwGMCXBhZ2Vfc2l6ZZRLFHVoJX2UaCePlChoSWhKaEhoB5BoKU51Yi4=", + "account__library_no_match.json": "gASVuQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwLTGlicmFyeUxpc3SUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwFaXRlbXOUXZSMBXRvdGFslEsAjARwYWdllEsBjAlwYWdlX3NpemWUSxR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgKaAtoCWgHkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViLg==", + "account__library_page_past_end.json": "gASVuQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwLTGlicmFyeUxpc3SUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwFaXRlbXOUXZSMBXRvdGFslEsBjARwYWdllEsCjAlwYWdlX3NpemWUSxR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgKaAtoCWgHkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViLg==", + "account__library_paginated_second_page.json": "gASVTAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwLTGlicmFyeUxpc3SUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwFaXRlbXOUXZQoaACMC0xpYnJhcnlJdGVtlJOUKYGUfZQoaAV9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBBQYWdlZCBjbGFpbSAwMjAulIwGZG9tYWlulIwAlIwIZW50aXRpZXOUXZSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMA2xvd5SMCmxlbnpfc2NvcmWUSwqMC2tleV9maW5kaW5nlIwuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpSMEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoEGgOaBRoGGggaBJoFmgjaCJoHWgaaBtoH5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgKKYGUfZQoaAV9lChoDowINGQ0NGRmYWaUaBCMEFBhZ2VkIGNsYWltIDAyMS6UaBJoE2gUXZRoFowEVHJ1ZZRoGIwDbG93lGgaSwpoG4wuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpRoHYwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpRoH05oIIwgMjAyNi0xMC0wMVQwOToxNDowMC4xMjM0NTYrMDA6MDCUaCJOaCOMAmVulHVoJX2UaCePlChoEGgOaBRoGGggaBJoFmgjaCJoHWgaaBtoH5BoKU51YmWMBXRvdGFslEsWjARwYWdllEsCjAlwYWdlX3NpemWUSxR1aCV9lGgnj5QoaDloOmg4aAeQaClOdWIu", + "account__me_usage_extra_credits.json": "gASVoAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMBGZyZWWUjApwbGFuX2xhYmVslIwERnJlZZSMD3F1b3RhX3Jlc2V0c19hdJSMGTIwMjYtMTAtMDFUMTQ6MDc6MDArMDA6MDCUjAdjcmVkaXRzlGgAjAxVc2FnZUNyZWRpdHOUk5QpgZR9lChoBX2UKIwFdG90YWyUTV4BjAR1c2VklEsAjAlyZW1haW5pbmeUTV4BjAVleHRyYZRL+owFYm9udXOUS/qMCXJlc2V0c19hdJSMGTIwMjYtMTAtMDFUMTQ6MDc6MDArMDA6MDCUdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoFGgYaBNoFWgWaBeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBWNvc3RzlH2UKIwGdmVyaWZ5lEsKjAZhc3Nlc3OUSwGMA2Fza5RLAYwIY2l0YXRpb26USwGMB2V4dHJhY3SUSwB1jAxjb3N0X29wdGlvbnOUfZRoIX2UjAVkZXB0aJR9lCiMCHN0YW5kYXJklEsKjANsb3eUSwV1c3OMBnZlcmlmeZRoAIwNVXNhZ2VDYXBhY2l0eZSTlCmBlH2UKGgFfZQojApxdW90YV91c2VklEsAjAtxdW90YV90b3RhbJRLI4wPcXVvdGFfcmVtYWluaW5nlEsjaBdLGWgNSxloFUsjdWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowDYXNrlGgvKYGUfZQoaAV9lChoM0sAaDRNXgFoNU1eAWgXS/poDUv6aBVNXgF1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijAZhc3Nlc3OUaC8pgZR9lChoBX2UKGgzSwBoNE1eAWg1TV4BaBdL+mgNS/poFU1eAXVoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMB2V4dHJhY3SUaACMDFVzYWdlRXh0cmFjdJSTlCmBlH2UKGgFfZQojAtjYWxsc190b2RheZRLAIwLZGFpbHlfbGltaXSUTegDjAl1bmxpbWl0ZWSUiXVoGn2UaByPlChoS2hMaEqQaB5OdWKMEmhhc193ZWJob29rX3NlY3JldJSJdWgafZRoHI+UKGgtaDhoDWg+aAdoC2gmaE9oH2hEaAmQaB5OdWIu", + "account__me_usage_extra_only.json": "gASVmAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMBGZyZWWUjApwbGFuX2xhYmVslIwERnJlZZSMD3F1b3RhX3Jlc2V0c19hdJSMGTIwMjYtMTAtMDFUMTQ6MDc6MDArMDA6MDCUjAdjcmVkaXRzlGgAjAxVc2FnZUNyZWRpdHOUk5QpgZR9lChoBX2UKIwFdG90YWyUS4mMBHVzZWSUS2SMCXJlbWFpbmluZ5RLJYwFZXh0cmGUSyWMBWJvbnVzlEsljAlyZXNldHNfYXSUjBkyMDI2LTEwLTAxVDE0OjA3OjAwKzAwOjAwlHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBRoGGgTaBVoFmgXkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAVjb3N0c5R9lCiMBnZlcmlmeZRLCowGYXNzZXNzlEsBjANhc2uUSwGMCGNpdGF0aW9ulEsBjAdleHRyYWN0lEsAdYwMY29zdF9vcHRpb25zlH2UaCF9lIwFZGVwdGiUfZQojAhzdGFuZGFyZJRLCowDbG93lEsFdXNzjAZ2ZXJpZnmUaACMDVVzYWdlQ2FwYWNpdHmUk5QpgZR9lChoBX2UKIwKcXVvdGFfdXNlZJRLCowLcXVvdGFfdG90YWyUSw2MD3F1b3RhX3JlbWFpbmluZ5RLA2gXSwNoDUsDaBVLA3VoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMA2Fza5RoLymBlH2UKGgFfZQoaDNLZGg0S4loNUslaBdLJWgNSyVoFUsldWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowGYXNzZXNzlGgvKYGUfZQoaAV9lChoM0tkaDRLiWg1SyVoF0slaA1LJWgVSyV1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijAdleHRyYWN0lGgAjAxVc2FnZUV4dHJhY3SUk5QpgZR9lChoBX2UKIwLY2FsbHNfdG9kYXmUSwCMC2RhaWx5X2xpbWl0lE3oA4wJdW5saW1pdGVklIl1aBp9lGgcj5QoaEtoTGhKkGgeTnVijBJoYXNfd2ViaG9va19zZWNyZXSUiXVoGn2UaByPlChoLWg4aA1oPmgHaAtoJmhPaB9oRGgJkGgeTnViLg==", + "account__me_usage_extract_calls.json": "gASVmAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMBGZyZWWUjApwbGFuX2xhYmVslIwERnJlZZSMD3F1b3RhX3Jlc2V0c19hdJSMGTIwMjYtMTAtMDFUMTQ6MDc6MDArMDA6MDCUjAdjcmVkaXRzlGgAjAxVc2FnZUNyZWRpdHOUk5QpgZR9lChoBX2UKIwFdG90YWyUS2SMBHVzZWSUSwCMCXJlbWFpbmluZ5RLZIwFZXh0cmGUSwCMBWJvbnVzlEsAjAlyZXNldHNfYXSUjBkyMDI2LTEwLTAxVDE0OjA3OjAwKzAwOjAwlHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBRoGGgTaBVoFmgXkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAVjb3N0c5R9lCiMBnZlcmlmeZRLCowGYXNzZXNzlEsBjANhc2uUSwGMCGNpdGF0aW9ulEsBjAdleHRyYWN0lEsAdYwMY29zdF9vcHRpb25zlH2UaCF9lIwFZGVwdGiUfZQojAhzdGFuZGFyZJRLCowDbG93lEsFdXNzjAZ2ZXJpZnmUaACMDVVzYWdlQ2FwYWNpdHmUk5QpgZR9lChoBX2UKIwKcXVvdGFfdXNlZJRLAIwLcXVvdGFfdG90YWyUSwqMD3F1b3RhX3JlbWFpbmluZ5RLCmgXSwBoDUsAaBVLCnVoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMA2Fza5RoLymBlH2UKGgFfZQoaDNLAGg0S2RoNUtkaBdLAGgNSwBoFUtkdWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowGYXNzZXNzlGgvKYGUfZQoaAV9lChoM0sAaDRLZGg1S2RoF0sAaA1LAGgVS2R1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijAdleHRyYWN0lGgAjAxVc2FnZUV4dHJhY3SUk5QpgZR9lChoBX2UKIwLY2FsbHNfdG9kYXmUSweMC2RhaWx5X2xpbWl0lE3oA4wJdW5saW1pdGVklIl1aBp9lGgcj5QoaEtoTGhKkGgeTnVijBJoYXNfd2ViaG9va19zZWNyZXSUiXVoGn2UaByPlChoLWg4aA1oPmgHaAtoJmhPaB9oRGgJkGgeTnViLg==", + "account__me_usage_free.json": "gASVmAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMBGZyZWWUjApwbGFuX2xhYmVslIwERnJlZZSMD3F1b3RhX3Jlc2V0c19hdJSMGTIwMjYtMTAtMDFUMTQ6MDc6MDArMDA6MDCUjAdjcmVkaXRzlGgAjAxVc2FnZUNyZWRpdHOUk5QpgZR9lChoBX2UKIwFdG90YWyUS2SMBHVzZWSUSwCMCXJlbWFpbmluZ5RLZIwFZXh0cmGUSwCMBWJvbnVzlEsAjAlyZXNldHNfYXSUjBkyMDI2LTEwLTAxVDE0OjA3OjAwKzAwOjAwlHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBRoGGgTaBVoFmgXkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAVjb3N0c5R9lCiMBnZlcmlmeZRLCowGYXNzZXNzlEsBjANhc2uUSwGMCGNpdGF0aW9ulEsBjAdleHRyYWN0lEsAdYwMY29zdF9vcHRpb25zlH2UaCF9lIwFZGVwdGiUfZQojAhzdGFuZGFyZJRLCowDbG93lEsFdXNzjAZ2ZXJpZnmUaACMDVVzYWdlQ2FwYWNpdHmUk5QpgZR9lChoBX2UKIwKcXVvdGFfdXNlZJRLAIwLcXVvdGFfdG90YWyUSwqMD3F1b3RhX3JlbWFpbmluZ5RLCmgXSwBoDUsAaBVLCnVoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMA2Fza5RoLymBlH2UKGgFfZQoaDNLAGg0S2RoNUtkaBdLAGgNSwBoFUtkdWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowGYXNzZXNzlGgvKYGUfZQoaAV9lChoM0sAaDRLZGg1S2RoF0sAaA1LAGgVS2R1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijAdleHRyYWN0lGgAjAxVc2FnZUV4dHJhY3SUk5QpgZR9lChoBX2UKIwLY2FsbHNfdG9kYXmUSwCMC2RhaWx5X2xpbWl0lE3oA4wJdW5saW1pdGVklIl1aBp9lGgcj5QoaEtoTGhKkGgeTnVijBJoYXNfd2ViaG9va19zZWNyZXSUiXVoGn2UaByPlChoLWg4aA1oPmgHaAtoJmhPaB9oRGgJkGgeTnViLg==", + "account__me_usage_free_partly_spent.json": "gASVmAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMBGZyZWWUjApwbGFuX2xhYmVslIwERnJlZZSMD3F1b3RhX3Jlc2V0c19hdJSMGTIwMjYtMTAtMDFUMTQ6MDc6MDArMDA6MDCUjAdjcmVkaXRzlGgAjAxVc2FnZUNyZWRpdHOUk5QpgZR9lChoBX2UKIwFdG90YWyUS2SMBHVzZWSUSxGMCXJlbWFpbmluZ5RLU4wFZXh0cmGUSwCMBWJvbnVzlEsAjAlyZXNldHNfYXSUjBkyMDI2LTEwLTAxVDE0OjA3OjAwKzAwOjAwlHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBRoGGgTaBVoFmgXkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAVjb3N0c5R9lCiMBnZlcmlmeZRLCowGYXNzZXNzlEsBjANhc2uUSwGMCGNpdGF0aW9ulEsBjAdleHRyYWN0lEsAdYwMY29zdF9vcHRpb25zlH2UaCF9lIwFZGVwdGiUfZQojAhzdGFuZGFyZJRLCowDbG93lEsFdXNzjAZ2ZXJpZnmUaACMDVVzYWdlQ2FwYWNpdHmUk5QpgZR9lChoBX2UKIwKcXVvdGFfdXNlZJRLAowLcXVvdGFfdG90YWyUSwqMD3F1b3RhX3JlbWFpbmluZ5RLCGgXSwBoDUsAaBVLCHVoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMA2Fza5RoLymBlH2UKGgFfZQoaDNLEWg0S2RoNUtTaBdLAGgNSwBoFUtTdWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowGYXNzZXNzlGgvKYGUfZQoaAV9lChoM0sRaDRLZGg1S1NoF0sAaA1LAGgVS1N1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijAdleHRyYWN0lGgAjAxVc2FnZUV4dHJhY3SUk5QpgZR9lChoBX2UKIwLY2FsbHNfdG9kYXmUSwCMC2RhaWx5X2xpbWl0lE3oA4wJdW5saW1pdGVklIl1aBp9lGgcj5QoaEtoTGhKkGgeTnVijBJoYXNfd2ViaG9va19zZWNyZXSUiXVoGn2UaByPlChoLWg4aA1oPmgHaAtoJmhPaB9oRGgJkGgeTnViLg==", + "account__me_usage_oauth.json": "gASVmAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMBGZyZWWUjApwbGFuX2xhYmVslIwERnJlZZSMD3F1b3RhX3Jlc2V0c19hdJSMGTIwMjYtMTAtMDFUMTQ6MDc6MDArMDA6MDCUjAdjcmVkaXRzlGgAjAxVc2FnZUNyZWRpdHOUk5QpgZR9lChoBX2UKIwFdG90YWyUS2SMBHVzZWSUSwCMCXJlbWFpbmluZ5RLZIwFZXh0cmGUSwCMBWJvbnVzlEsAjAlyZXNldHNfYXSUjBkyMDI2LTEwLTAxVDE0OjA3OjAwKzAwOjAwlHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBRoGGgTaBVoFmgXkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAVjb3N0c5R9lCiMBnZlcmlmeZRLCowGYXNzZXNzlEsBjANhc2uUSwGMCGNpdGF0aW9ulEsBjAdleHRyYWN0lEsAdYwMY29zdF9vcHRpb25zlH2UaCF9lIwFZGVwdGiUfZQojAhzdGFuZGFyZJRLCowDbG93lEsFdXNzjAZ2ZXJpZnmUaACMDVVzYWdlQ2FwYWNpdHmUk5QpgZR9lChoBX2UKIwKcXVvdGFfdXNlZJRLAIwLcXVvdGFfdG90YWyUSwqMD3F1b3RhX3JlbWFpbmluZ5RLCmgXSwBoDUsAaBVLCnVoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMA2Fza5RoLymBlH2UKGgFfZQoaDNLAGg0S2RoNUtkaBdLAGgNSwBoFUtkdWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowGYXNzZXNzlGgvKYGUfZQoaAV9lChoM0sAaDRLZGg1S2RoF0sAaA1LAGgVS2R1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijAdleHRyYWN0lGgAjAxVc2FnZUV4dHJhY3SUk5QpgZR9lChoBX2UKIwLY2FsbHNfdG9kYXmUSwCMC2RhaWx5X2xpbWl0lE3oA4wJdW5saW1pdGVklIl1aBp9lGgcj5QoaEtoTGhKkGgeTnVijBJoYXNfd2ViaG9va19zZWNyZXSUiXVoGn2UaByPlChoLWg4aA1oPmgHaAtoJmhPaB9oRGgJkGgeTnViLg==", + "account__me_usage_plus.json": "gASVrgMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMBHBsdXOUjApwbGFuX2xhYmVslIwEUGx1c5SMD3F1b3RhX3Jlc2V0c19hdJSMIDIwMjYtMTAtMDFUMTQ6MDc6MDAuMTIzNDU2KzAwOjAwlIwHY3JlZGl0c5RoAIwMVXNhZ2VDcmVkaXRzlJOUKYGUfZQoaAV9lCiMBXRvdGFslE30AYwEdXNlZJRLAIwJcmVtYWluaW5nlE30AYwFZXh0cmGUSwCMBWJvbnVzlEsAjAlyZXNldHNfYXSUjCAyMDI2LTEwLTAxVDE0OjA3OjAwLjEyMzQ1NiswMDowMJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgUaBhoE2gVaBZoF5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowFY29zdHOUfZQojAZ2ZXJpZnmUSwqMBmFzc2Vzc5RLAYwDYXNrlEsBjAhjaXRhdGlvbpRLAYwHZXh0cmFjdJRLAHWMDGNvc3Rfb3B0aW9uc5R9lGghfZSMBWRlcHRolH2UKIwIc3RhbmRhcmSUSwqMA2xvd5RLBXVzc4wGdmVyaWZ5lGgAjA1Vc2FnZUNhcGFjaXR5lJOUKYGUfZQoaAV9lCiMCnF1b3RhX3VzZWSUSwCMC3F1b3RhX3RvdGFslEsyjA9xdW90YV9yZW1haW5pbmeUSzJoF0sAaA1LAGgVSzJ1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijANhc2uUaC8pgZR9lChoBX2UKGgzSwBoNE30AWg1TfQBaBdLAGgNSwBoFU30AXVoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMBmFzc2Vzc5RoLymBlH2UKGgFfZQoaDNLAGg0TfQBaDVN9AFoF0sAaA1LAGgVTfQBdWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowHZXh0cmFjdJRoAIwMVXNhZ2VFeHRyYWN0lJOUKYGUfZQoaAV9lCiMC2NhbGxzX3RvZGF5lEsAjAtkYWlseV9saW1pdJRN6AOMCXVubGltaXRlZJSJdWgafZRoHI+UKGhLaExoSpBoHk51YowSaGFzX3dlYmhvb2tfc2VjcmV0lIl1aBp9lGgcj5QoaC1oOGgNaD5oB2gLaCZoT2gfaERoCZBoHk51Yi4=", + "account__me_usage_pro.json": "gASVrwMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMA3Byb5SMCnBsYW5fbGFiZWyUjANQcm+UjA9xdW90YV9yZXNldHNfYXSUjCAyMDI2LTEwLTAxVDE0OjA3OjAwLjEyMzQ1NiswMDowMJSMB2NyZWRpdHOUaACMDFVzYWdlQ3JlZGl0c5STlCmBlH2UKGgFfZQojAV0b3RhbJRNiBOMBHVzZWSUSwCMCXJlbWFpbmluZ5RNiBOMBWV4dHJhlEsAjAVib251c5RLAIwJcmVzZXRzX2F0lIwgMjAyNi0xMC0wMVQxNDowNzowMC4xMjM0NTYrMDA6MDCUdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoFGgYaBNoFWgWaBeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBWNvc3RzlH2UKIwGdmVyaWZ5lEsKjAZhc3Nlc3OUSwGMA2Fza5RLAYwIY2l0YXRpb26USwGMB2V4dHJhY3SUSwB1jAxjb3N0X29wdGlvbnOUfZRoIX2UjAVkZXB0aJR9lCiMCHN0YW5kYXJklEsKjANsb3eUSwV1c3OMBnZlcmlmeZRoAIwNVXNhZ2VDYXBhY2l0eZSTlCmBlH2UKGgFfZQojApxdW90YV91c2VklEsAjAtxdW90YV90b3RhbJRN9AGMD3F1b3RhX3JlbWFpbmluZ5RN9AFoF0sAaA1LAGgVTfQBdWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowDYXNrlGgvKYGUfZQoaAV9lChoM0sAaDRNiBNoNU2IE2gXSwBoDUsAaBVNiBN1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijAZhc3Nlc3OUaC8pgZR9lChoBX2UKGgzSwBoNE2IE2g1TYgTaBdLAGgNSwBoFU2IE3VoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMB2V4dHJhY3SUaACMDFVzYWdlRXh0cmFjdJSTlCmBlH2UKGgFfZQojAtjYWxsc190b2RheZRLAIwLZGFpbHlfbGltaXSUTegDjAl1bmxpbWl0ZWSUiXVoGn2UaByPlChoS2hMaEqQaB5OdWKMEmhhc193ZWJob29rX3NlY3JldJSJdWgafZRoHI+UKGgtaDhoDWg+aAdoC2gmaE9oH2hEaAmQaB5OdWIu", + "account__me_usage_pro_extra.json": "gASVtQMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMA3Byb5SMCnBsYW5fbGFiZWyUjANQcm+UjA9xdW90YV9yZXNldHNfYXSUjCAyMDI2LTEwLTAxVDE0OjA3OjAwLjEyMzQ1NiswMDowMJSMB2NyZWRpdHOUaACMDFVzYWdlQ3JlZGl0c5STlCmBlH2UKGgFfZQojAV0b3RhbJRNcBeMBHVzZWSUSwCMCXJlbWFpbmluZ5RNcBeMBWV4dHJhlE3oA4wFYm9udXOUTegDjAlyZXNldHNfYXSUjCAyMDI2LTEwLTAxVDE0OjA3OjAwLjEyMzQ1NiswMDowMJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgUaBhoE2gVaBZoF5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowFY29zdHOUfZQojAZ2ZXJpZnmUSwqMBmFzc2Vzc5RLAYwDYXNrlEsBjAhjaXRhdGlvbpRLAYwHZXh0cmFjdJRLAHWMDGNvc3Rfb3B0aW9uc5R9lGghfZSMBWRlcHRolH2UKIwIc3RhbmRhcmSUSwqMA2xvd5RLBXVzc4wGdmVyaWZ5lGgAjA1Vc2FnZUNhcGFjaXR5lJOUKYGUfZQoaAV9lCiMCnF1b3RhX3VzZWSUSwCMC3F1b3RhX3RvdGFslE1YAowPcXVvdGFfcmVtYWluaW5nlE1YAmgXS2RoDUtkaBVNWAJ1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijANhc2uUaC8pgZR9lChoBX2UKGgzSwBoNE1wF2g1TXAXaBdN6ANoDU3oA2gVTXAXdWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowGYXNzZXNzlGgvKYGUfZQoaAV9lChoM0sAaDRNcBdoNU1wF2gXTegDaA1N6ANoFU1wF3VoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMB2V4dHJhY3SUaACMDFVzYWdlRXh0cmFjdJSTlCmBlH2UKGgFfZQojAtjYWxsc190b2RheZRLAIwLZGFpbHlfbGltaXSUTegDjAl1bmxpbWl0ZWSUiXVoGn2UaByPlChoS2hMaEqQaB5OdWKMEmhhc193ZWJob29rX3NlY3JldJSJdWgafZRoHI+UKGgtaDhoDWg+aAdoC2gmaE9oH2hEaAmQaB5OdWIu", + "account__me_usage_stale_quota_row.json": "gASVmAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwFVXNhZ2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwEcGxhbpSMBGZyZWWUjApwbGFuX2xhYmVslIwERnJlZZSMD3F1b3RhX3Jlc2V0c19hdJSMGTIwMjYtMTAtMDFUMTQ6MDc6MDArMDA6MDCUjAdjcmVkaXRzlGgAjAxVc2FnZUNyZWRpdHOUk5QpgZR9lChoBX2UKIwFdG90YWyUS2SMBHVzZWSUSwCMCXJlbWFpbmluZ5RLZIwFZXh0cmGUSwCMBWJvbnVzlEsAjAlyZXNldHNfYXSUjBkyMDI2LTEwLTAxVDE0OjA3OjAwKzAwOjAwlHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBRoGGgTaBVoFmgXkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAVjb3N0c5R9lCiMBnZlcmlmeZRLCowGYXNzZXNzlEsBjANhc2uUSwGMCGNpdGF0aW9ulEsBjAdleHRyYWN0lEsAdYwMY29zdF9vcHRpb25zlH2UaCF9lIwFZGVwdGiUfZQojAhzdGFuZGFyZJRLCowDbG93lEsFdXNzjAZ2ZXJpZnmUaACMDVVzYWdlQ2FwYWNpdHmUk5QpgZR9lChoBX2UKIwKcXVvdGFfdXNlZJRLAIwLcXVvdGFfdG90YWyUSwqMD3F1b3RhX3JlbWFpbmluZ5RLCmgXSwBoDUsAaBVLCnVoGn2UaByPlChoDWgzaDRoFWg1aBeQaB5OdWKMA2Fza5RoLymBlH2UKGgFfZQoaDNLAGg0S2RoNUtkaBdLAGgNSwBoFUtkdWgafZRoHI+UKGgNaDNoNGgVaDVoF5BoHk51YowGYXNzZXNzlGgvKYGUfZQoaAV9lChoM0sAaDRLZGg1S2RoF0sAaA1LAGgVS2R1aBp9lGgcj5QoaA1oM2g0aBVoNWgXkGgeTnVijAdleHRyYWN0lGgAjAxVc2FnZUV4dHJhY3SUk5QpgZR9lChoBX2UKIwLY2FsbHNfdG9kYXmUSwCMC2RhaWx5X2xpbWl0lE3oA4wJdW5saW1pdGVklIl1aBp9lGgcj5QoaEtoTGhKkGgeTnVijBJoYXNfd2ViaG9va19zZWNyZXSUiXVoGn2UaByPlChoLWg4aA1oPmgHaAtoJmhPaB9oRGgJkGgeTnViLg==", + "assess__deep_tier_hit.json": "gASVHwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwUV2F0ZXIgYm9pbHMgYXQgMTAwQy6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUjC1odHRwczovL2xlbnouaW8vYXBpL3YxL3ZlcmlmaWNhdGlvbnMvZjM0MzZmNTCUjAlyYXRpb25hbGWUjBZUaGUgY2xhaW0gaXMgdmVyaWZpZWQulIwHZGlzc2VudJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCFoGmgdaBZoEGgUaB9oEmgYaBxoG5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oHIwAlGgdXZSMC21vcmVfY2xhaW1zlF2UdWgifZRoJI+UKGgHaCpoJ5BoJk51Yi4=", + "assess__idempotency_first.json": "gASV3QEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwZVGhlIEVhcnRoIG9yYml0cyB0aGUgU3VuLpSMCGxhbmd1YWdllIwCZW6UjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwQdmVyaWZpY2F0aW9uX3VybJROjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UdWggfZRoIo+UKGgHaChoJZBoJE51Yi4=", + "assess__idempotency_replay.json": "gASV3QEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwZVGhlIEVhcnRoIG9yYml0cyB0aGUgU3VuLpSMCGxhbmd1YWdllIwCZW6UjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwQdmVyaWZpY2F0aW9uX3VybJROjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UdWggfZRoIo+UKGgHaChoJZBoJE51Yi4=", + "assess__list_all_error_rows.json": "gASVJQMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMAmhplIwIbGFuZ3VhZ2WUjAJlbpSMB3ZlcmRpY3SUjAVFcnJvcpSMCmNvbmZpZGVuY2WUjANsb3eUjBB2ZXJpZmljYXRpb25fdXJslE6MCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmVycm9yX2NvZGWUjAhub19jbGFpbZSMEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lIxqVGhlIGlucHV0IGlzIGEgZ3JlZXRpbmcuIFNlbmQgb25lIGZhY3R1YWwgY2xhaW0sIG9yIHJ1biB0aGUgdGV4dCB0aHJvdWdoIC9leHRyYWN0IHRvIGVudW1lcmF0ZSBpdHMgY2xhaW1zLpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCBoGGgcaBZoEGgUaB5oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgKKYGUfZQoaAV9lChoDowFaGVsbG+UaBCMAmVulGgSjAVFcnJvcpRoFIwDbG93lGgWTmgXTmgYTmgZTmgajAhub19jbGFpbZRoHF2UaB5dlGggjGpUaGUgaW5wdXQgaXMgYSBncmVldGluZy4gU2VuZCBvbmUgZmFjdHVhbCBjbGFpbSwgb3IgcnVuIHRoZSB0ZXh0IHRocm91Z2ggL2V4dHJhY3QgdG8gZW51bWVyYXRlIGl0cyBjbGFpbXMulHVoIn2UaCSPlChoDmggaBhoHGgWaBBoFGgeaBJoF2gaaBmQaCZOdWJljAVlcnJvcpROaBqMAJRoHF2UjAttb3JlX2NsYWltc5RdlHVoIn2UaCSPlChoB2g3aDSQaCZOdWIu", + "assess__list_compound_item.json": "gASV2wIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMDlByaW1hcnkgY2xhaW0ulIwIbGFuZ3VhZ2WUjAJlbpSMB3ZlcmRpY3SUjAxNb3N0bHkgRmFsc2WUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTowKZXJyb3JfY29kZZROjBBjYW5kaWRhdGVfY2xhaW1zlF2UjBFpZGVudGlmaWVkX2NsYWltc5RdlCiMDVNlY29uZCBjbGFpbS6UjAxUaGlyZCBjbGFpbS6UZYwEaGludJSMWkFzc2Vzc2VkIHRoZSBtYWluIGNsYWltIG9ubHkuIFNlbmQgaWRlbnRpZmllZF9jbGFpbXMgYXMgdGhlaXIgb3duIGl0ZW1zIHRvIGNoZWNrIHRoZSByZXN0LpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCFoGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgKKYGUfZQoaAV9lChoDowOQSBwbGFpbiBjbGFpbS6UaBCMAmVulGgSjAxNb3N0bHkgRmFsc2WUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoIU51aCN9lGglj5QoaA5oIWgYaBtoFmgQaBRoHWgSaBdoGmgZkGgnTnViZYwFZXJyb3KUTmgajACUaBtdlIwLbW9yZV9jbGFpbXOUXZR1aCN9lGglj5QoaAdoNmgzkGgnTnViLg==", + "assess__list_mixed_rows.json": "gASVxAQAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMIldhdGVyIGJvaWxzIGF0IDEwMCBDIGF0IHNlYSBsZXZlbC6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTowKZXJyb3JfY29kZZROjBBjYW5kaWRhdGVfY2xhaW1zlF2UjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoCimBlH2UKGgFfZQoaA6MC2hlbGxvIHRoZXJllGgQjAJlbpRoEowFRXJyb3KUaBSMA2xvd5RoFk5oF05oGE5oGU5oGowIbm9fY2xhaW2UaBtdlGgdXZRoH4xqVGhlIGlucHV0IGlzIGEgZ3JlZXRpbmcuIFNlbmQgb25lIGZhY3R1YWwgY2xhaW0sIG9yIHJ1biB0aGUgdGV4dCB0aHJvdWdoIC9leHRyYWN0IHRvIGVudW1lcmF0ZSBpdHMgY2xhaW1zLpR1aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjA52ZW5kb3IgaXMgZG93bpRoEIwCZW6UaBKMBUVycm9ylGgUjANsb3eUaBZOaBdOaBhOaBlOaBqMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGgbXZRoHV2UaB+MTkEgbW9kZWwgcHJvdmlkZXIgd2FzIHVuYXZhaWxhYmxlIGZvciB0aGlzIGl0ZW0uIFJldHJ5IGl0OyBub3RoaW5nIHdhcyBjaGFyZ2VkLpR1aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjAxjYW5ub3QgZnJhbWWUaBCMAmVulGgSjAVFcnJvcpRoFIwDbG93lGgWTmgXTmgYTmgZTmgajA5mcmFtaW5nX2ZhaWxlZJRoG12UaB1dlGgfjINUaGlzIGl0ZW0gY291bGQgbm90IGJlIHByb2Nlc3NlZDsgcmV0cnlpbmcgaXQgYXMtaXMgd2lsbCBub3QgaGVscC4gU2VuZCBpdCByZXBocmFzZWQgYXMgb25lIGZhY3R1YWwgc3RhdGVtZW50OyBub3RoaW5nIHdhcyBjaGFyZ2VkLpR1aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViZYwFZXJyb3KUTmgajACUaBtdlIwLbW9yZV9jbGFpbXOUXZR1aCB9lGgij5QoaAdoT2hMkGgkTnViLg==", + "assess__memo_hit.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwgVGhlIERhbnViZSBmbG93cyB0aHJvdWdoIFZpZW5uYS6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllIwgVGhlIHJpdmVyIHJ1bnMgdGhyb3VnaCB0aGUgY2l0eS6UjAdkaXNzZW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmVycm9yX2NvZGWUTowQY2FuZGlkYXRlX2NsYWltc5RdlIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA5oIGgZaBxoFmgQaBRoHmgSaBdoG2gakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViYYwFZXJyb3KUTmgbjACUaBxdlIwLbW9yZV9jbGFpbXOUXZR1aCF9lGgjj5QoaAdoKWgmkGglTnViLg==", + "assess__replay_later_200.json": "gASV3QEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwZVGhlIEVhcnRoIG9yYml0cyB0aGUgU3VuLpSMCGxhbmd1YWdllIwCZW6UjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwQdmVyaWZpY2F0aW9uX3VybJROjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UdWggfZRoIo+UKGgHaChoJZBoJE51Yi4=", + "assess__single_no_claim.json": "gASVAgEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UjAVlcnJvcpSMHE5vIHZlcmlmaWFibGUgY2xhaW0gZGV0ZWN0ZWSUjAplcnJvcl9jb2RllIwIbm9fY2xhaW2UjBBjYW5kaWRhdGVfY2xhaW1zlF2UjAttb3JlX2NsYWltc5RdlHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA1oD2gLaAdoCZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "assess__single_one_claim.json": "gASViwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwsVGhlIHJlZ2lzdHJ5IHJlcG9ydGVkIDQsMjAwIGZpbGluZ3MgaW4gMjAyNC6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllIxJVGhlIHJlZ2lzdHJ5IGxpc3RzIDQsMjAwIGZpbGluZ3MgZm9yIDIwMjQgYW5kIGhhcyBub3QgcmV2aXNlZCB0aGUgZmlndXJlLpSMB2Rpc3NlbnSUjE5UaGUgcmVnaXN0cnkgZmlndXJlIGlzIHByb3Zpc2lvbmFsLCBzbyB0aGUgMjAyNCB0b3RhbCBjYW5ub3QgeWV0IGJlIGNvbmZpcm1lZC6UjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCFoGWgdaBZoEGgUaB9oEmgXaBxoG5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oHIwAlGgdXZSMC21vcmVfY2xhaW1zlF2UdWgifZRoJI+UKGgHaCpoJ5BoJk51Yi4=", + "assess__single_text_over_wave_more_claims.json": "gASVbgwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMHUNsYWltIG51bWJlciAwIGlzIGRvY3VtZW50ZWQulIwIbGFuZ3VhZ2WUjAJlbpSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjBB2ZXJpZmljYXRpb25fdXJslE6MCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmVycm9yX2NvZGWUTowQY2FuZGlkYXRlX2NsYWltc5RdlIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgMSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgMiBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgMyBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNCBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNiBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNyBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgOCBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgOSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTAgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDoweQ2xhaW0gbnVtYmVyIDExIGlzIGRvY3VtZW50ZWQulGgQjAJlbpRoEowEVHJ1ZZRoFIwEaGlnaJRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJoCimBlH2UKGgFfZQoaA6MHkNsYWltIG51bWJlciAxMiBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTMgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDoweQ2xhaW0gbnVtYmVyIDE0IGlzIGRvY3VtZW50ZWQulGgQjAJlbpRoEowEVHJ1ZZRoFIwEaGlnaJRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJoCimBlH2UKGgFfZQoaA6MHkNsYWltIG51bWJlciAxNSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTYgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDoweQ2xhaW0gbnVtYmVyIDE3IGlzIGRvY3VtZW50ZWQulGgQjAJlbpRoEowEVHJ1ZZRoFIwEaGlnaJRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJoCimBlH2UKGgFfZQoaA6MHkNsYWltIG51bWJlciAxOCBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTkgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmWMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UKIweQ2xhaW0gbnVtYmVyIDIwIGlzIGRvY3VtZW50ZWQulIweQ2xhaW0gbnVtYmVyIDIxIGlzIGRvY3VtZW50ZWQulGV1aCB9lGgij5QoaAdo+Wj2kGgkTnViLg==", + "assess__single_text_several_claims.json": "gASVBwMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMIldhdGVyIGJvaWxzIGF0IDEwMCBDIGF0IHNlYSBsZXZlbC6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTowKZXJyb3JfY29kZZROjBBjYW5kaWRhdGVfY2xhaW1zlF2UjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoCimBlH2UKGgFfZQoaA6MG1RoZSBNb29uIGlzIG1hZGUgb2YgY2hlZXNlLpRoEIwCZW6UaBKMBUZhbHNllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDowhVGhlIFBhY2lmaWMgaXMgdGhlIGRlZXBlc3Qgb2NlYW4ulGgQjAJlbpRoEowLTW9zdGx5IFRydWWUaBSMBm1lZGl1bZRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJljAVlcnJvcpROaBqMAJRoG12UjAttb3JlX2NsYWltc5RdlHVoIH2UaCKPlChoB2g+aDuQaCROdWIu", + "assess__suggest_rewrite.json": "gASVBwMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMJ1ZlbnVzIGlzIHRoZSBjbG9zZXN0IHBsYW5ldCB0byB0aGUgU3VuLpSMCGxhbmd1YWdllIwCZW6UjAd2ZXJkaWN0lIwFRmFsc2WUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllIw1TWVyY3VyeSwgbm90IFZlbnVzLCBpcyB0aGUgY2xvc2VzdCBwbGFuZXQgdG8gdGhlIFN1bi6UjAdkaXNzZW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllIwpTWVyY3VyeSBpcyB0aGUgY2xvc2VzdCBwbGFuZXQgdG8gdGhlIFN1bi6UjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCFoGWgdaBZoEGgUaB9oEmgXaBxoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgKKYGUfZQoaAV9lChoDowpTWVyY3VyeSBpcyB0aGUgY2xvc2VzdCBwbGFuZXQgdG8gdGhlIFN1bi6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXjCBNZXJjdXJ5IGlzIHRoZSBpbm5lcm1vc3QgcGxhbmV0LpRoGU5oGk5oHE5oHV2UaB9dlGghTnVoIn2UaCSPlChoDmghaBloHWgWaBBoFGgfaBJoF2gcaBqQaCZOdWJljAVlcnJvcpROaByMAJRoHV2UjAttb3JlX2NsYWltc5RdlHVoIn2UaCSPlChoB2g2aDOQaCZOdWIu", + "citecheck__get_checking.json": "gASV7AYAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwIY2hlY2tpbmeUjAdvdXRjb21llE6MCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUTowScG9sbF9hZnRlcl9zZWNvbmRzlEsKjAZwb2xpY3mUaACMD0NpdGVjaGVja1BvbGljeZSTlCmBlH2UKGgFfZSMDW1heF9jaXRhdGlvbnOUSwJzjBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgWkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjBBDaXRlY2hlY2tTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NpdGF0aW9uc19mb3VuZJRLAowSY2l0YXRpb25zX3NlbGVjdGVklEsCjA5jaXRhdGlvbl9saW1pdJRLAowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJSJjA9jaXRhdGlvbl9jaGVja3OUaACMGVJldmlld0NpdGF0aW9uQ2hlY2tDb3VudHOUk5QpgZR9lChoBX2UKIwHY2hlY2tlZJRLAIwJdW5jaGVja2VklEsAjAZmYWlsZWSUSwB1aBd9lGgZj5QoaCxoLWgukGgbTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwB1aBd9lGgZj5QoaCNoJWgxaCRoJmgikGgbTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAHNoF32UaBmPlChoOpBoG051YowJY2l0YXRpb25zlF2UKGgAjA5SZXZpZXdDaXRhdGlvbpSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwJcmVmZXJlbmNllIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIwiVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LpSMBnF1b3Rlc5RdlIwIcG9zaXRpb26UTowGcmVzdWx0lE6MBWNoZWNrlGgAjBNSZXZpZXdDaXRhdGlvbkNoZWNrlJOUKYGUfZQoaAV9lChoCYwHcGVuZGluZ5SMCXBhZ2VfcmVhZJROjApwYWdlX3RpdGxllE6ME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZROjApzb3VyY2VfdXJslE6MDnNvdXJjZV92ZXJzaW9ulE6MB3N1cHBvcnSUTowHc25pcHBldJROjAlyYXRpb25hbGWUTowFcXVvdGWUTowNbWlzc2luZ19xdW90ZZROjA5kb2lfcmVnaXN0ZXJlZJROjAhtZXRhZGF0YZROjBRtZXRhZGF0YV9kaWZmZXJlbmNlc5RdlIwKcmVnaXN0ZXJlZJROjBB1bmNoZWNrZWRfcmVhc29ulE6MBGhpbnSUTowHZmFpbHVyZZROdWgXfZRoGY+UKGhZaGdoX2haaFtoaGhiaGNoCWhhaGRoYGhYaGZoXWhXaFxoXmhpkGgbTnVidWgXfZRoGY+UKGhKaE9oRWhQaERoSWhMaEdoTpBoG051YmhAKYGUfZQoaAV9lChoREsBaEWMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhHjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRoSU5oSowiVGhlIGFnZW5jeSBzYWlkIHNvIGluIGEgc3RhdGVtZW50LpRoTF2UaE5OaE9OaFBoUimBlH2UKGgFfZQoaAmMB3BlbmRpbmeUaFdOaFhOaFlOaFpOaFtOaFxOaF1OaF5OaF9OaGBOaGFOaGJOaGNOaGRdlGhmTmhnTmhoTmhpTnVoF32UaBmPlChoWWhnaF9oWmhbaGhoYmhjaAloYWhkaGBoWGhmaF1oV2hcaF5oaZBoG051YnVoF32UaBmPlChoSmhPaEVoUGhEaEloTGhHaE6QaBtOdWJlaDFdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaGlOdWgXfZSMCGxhbmd1YWdllIwCZW6Uc2gZj5QoaAtoNGgOaAxohGgJaBBoPWgxaH9oD2gHaBxogWhpkGgbTnViLg==", + "citecheck__get_completed_clean.json": "gASV0QgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROjAZwb2xpY3mUaACMD0NpdGVjaGVja1BvbGljeZSTlCmBlH2UKGgFfZSMDW1heF9jaXRhdGlvbnOUSwJzjBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjBBDaXRlY2hlY2tTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NpdGF0aW9uc19mb3VuZJRLAowSY2l0YXRpb25zX3NlbGVjdGVklEsCjA5jaXRhdGlvbl9saW1pdJRLAowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJSJjA9jaXRhdGlvbl9jaGVja3OUaACMGVJldmlld0NpdGF0aW9uQ2hlY2tDb3VudHOUk5QpgZR9lChoBX2UKIwHY2hlY2tlZJRLAowJdW5jaGVja2VklEsAjAZmYWlsZWSUSwB1aBl9lGgbj5QoaC5oL2gwkGgdTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwB1aBl9lGgbj5QoaCVoJ2gzaCZoKGgkkGgdTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoGX2UaBuPlChoPJBoHU51YowJY2l0YXRpb25zlF2UKGgAjA5SZXZpZXdDaXRhdGlvbpSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwJcmVmZXJlbmNllIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIwiVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LpSMBnF1b3Rlc5RdlIwIcG9zaXRpb26UTowGcmVzdWx0lGgAjBRSZXZpZXdDaXRhdGlvblJlc3VsdJSTlCmBlH2UKGgFfZQojAdmaW5kaW5nlIwJc3VwcG9ydGVklIwGc291cmNllIwHc3VwcG9ydJSMCGlzX2lzc3VllIl1aBl9lGgbj5QoaFdoW2hZkGgdTnVijAVjaGVja5RoAIwTUmV2aWV3Q2l0YXRpb25DaGVja5STlCmBlH2UKGgFfZQoaAmMCWNvbXBsZXRlZJSMCXBhZ2VfcmVhZJSMBGZ1bGyUjApwYWdlX3RpdGxllIwnUGFnZSBhdCBodHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwTcGFnZV9wdWJsaXNoZWRfZGF0ZZROjA1wYWdlX2xhbmd1YWdllIwCZW6UjApzb3VyY2VfdXJslIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMDnNvdXJjZV92ZXJzaW9ulE6MB3N1cHBvcnSUjAlzdXBwb3J0ZWSUjAdzbmlwcGV0lIwTVGhlIHNvdXJjZSBzYXlzIHNvLpSMCXJhdGlvbmFsZZSME1RoZSBwYWdlIHN0YXRlcyBpdC6UjAVxdW90ZZROjA1taXNzaW5nX3F1b3RllE6MDmRvaV9yZWdpc3RlcmVklE6MCG1ldGFkYXRhlE6MFG1ldGFkYXRhX2RpZmZlcmVuY2VzlF2UjApyZWdpc3RlcmVklE6MEHVuY2hlY2tlZF9yZWFzb26UTowEaGludJROjAdmYWlsdXJllE51aBl9lGgbj5QoaGlofGhzaGpobGh9aHdoeGgJaHZoeWh1aGdoe2hvaGVobmhxaH6QaB1OdWJ1aBl9lGgbj5QoaExoUWhHaF5oRmhLaE5oSWhQkGgdTnViaEIpgZR9lChoBX2UKGhGSwFoR4wdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaEmMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhLTmhMjCJUaGUgYWdlbmN5IHNhaWQgc28gaW4gYSBzdGF0ZW1lbnQulGhOXZRoUE5oUWhTKYGUfZQoaAV9lChoV4wJc3VwcG9ydGVklGhZjAdzdXBwb3J0lGhbiXVoGX2UaBuPlChoV2hbaFmQaB1OdWJoXmhgKYGUfZQoaAV9lChoCYwJY29tcGxldGVklGhljARmdWxslGhnjCVQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhpTmhqjAJlbpRobIwdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaG5OaG+MCXN1cHBvcnRlZJRocYwTVGhlIHNvdXJjZSBzYXlzIHNvLpRoc4wTVGhlIHBhZ2Ugc3RhdGVzIGl0LpRodU5odk5od05oeE5oeV2UaHtOaHxOaH1OaH5OdWgZfZRoG4+UKGhpaHxoc2hqaGxofWh3aHhoCWh2aHlodWhnaHtob2hlaG5ocWh+kGgdTnVidWgZfZRoG4+UKGhMaFFoR2heaEZoS2hOaEloUJBoHU51YmVoM12UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRofk51aBl9lIwIbGFuZ3VhZ2WUjAJlbpRzaBuPlChoC2g2aA9oDWinaAloEmg/aDNoomgRaAdoHmikaH6QaB1OdWIu", + "citecheck__get_completed_doi_pair.json": "gASVrAYAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROjAZwb2xpY3mUaACMD0NpdGVjaGVja1BvbGljeZSTlCmBlH2UKGgFfZSMDW1heF9jaXRhdGlvbnOUSwFzjBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjBBDaXRlY2hlY2tTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NpdGF0aW9uc19mb3VuZJRLAYwSY2l0YXRpb25zX3NlbGVjdGVklEsBjA5jaXRhdGlvbl9saW1pdJRLAYwWY2l0YXRpb25fbGltaXRfcmVhY2hlZJSJjA9jaXRhdGlvbl9jaGVja3OUaACMGVJldmlld0NpdGF0aW9uQ2hlY2tDb3VudHOUk5QpgZR9lChoBX2UKIwHY2hlY2tlZJRLAYwJdW5jaGVja2VklEsAjAZmYWlsZWSUSwB1aBl9lGgbj5QoaC5oL2gwkGgdTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwB1aBl9lGgbj5QoaCVoJ2gzaCZoKGgkkGgdTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAXNoGX2UaBuPlChoPJBoHU51YowJY2l0YXRpb25zlF2UaACMDlJldmlld0NpdGF0aW9ulJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAjAlyZWZlcmVuY2WUjBMxMC4xMDM4L25hdHVyZTEyMzczlIwJY2l0ZWRfdXJslIwjaHR0cHM6Ly9kb2kub3JnLzEwLjEwMzgvbmF0dXJlMTIzNzOUjANkb2mUjBMxMC4xMDM4L25hdHVyZTEyMzczlIwJc3RhdGVtZW50lIwmRGlhbW9uZCBzZW5zb3JzIG1lYXN1cmUgaGVhdCBpbiBjZWxscy6UjAZxdW90ZXOUXZSMCHBvc2l0aW9ulE6MBnJlc3VsdJRoAIwUUmV2aWV3Q2l0YXRpb25SZXN1bHSUk5QpgZR9lChoBX2UKIwHZmluZGluZ5SMCXN1cHBvcnRlZJSMBnNvdXJjZZSMB3N1cHBvcnSUjAhpc19pc3N1ZZSJdWgZfZRoG4+UKGhYaFxoWpBoHU51YowFY2hlY2uUaACME1Jldmlld0NpdGF0aW9uQ2hlY2uUk5QpgZR9lChoBX2UKGgJjAljb21wbGV0ZWSUjAlwYWdlX3JlYWSUjARmdWxslIwKcGFnZV90aXRsZZSMDFBhZ2UgYXQgTm9uZZSME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZSMAmVulIwKc291cmNlX3VybJROjA5zb3VyY2VfdmVyc2lvbpROjAdzdXBwb3J0lIwJc3VwcG9ydGVklIwHc25pcHBldJSME1RoZSBzb3VyY2Ugc2F5cyBzby6UjAlyYXRpb25hbGWUjBNUaGUgcGFnZSBzdGF0ZXMgaXQulIwFcXVvdGWUTowNbWlzc2luZ19xdW90ZZROjA5kb2lfcmVnaXN0ZXJlZJROjAhtZXRhZGF0YZROjBRtZXRhZGF0YV9kaWZmZXJlbmNlc5RdlIwKcmVnaXN0ZXJlZJROjBB1bmNoZWNrZWRfcmVhc29ulE6MBGhpbnSUTowHZmFpbHVyZZROdWgZfZRoG4+UKGhqaHxoc2hraG1ofWh3aHhoCWh2aHlodWhoaHtob2hmaG5ocWh+kGgdTnVidWgZfZRoG4+UKGhNaFJoR2hfaEZoS2hPaEloUZBoHU51YmFoM12UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRofk51aBl9lIwIbGFuZ3VhZ2WUjAJlbpRzaBuPlChoC2g2aA9oDWiJaAloEmg/aDNohGgRaAdoHmiGaH6QaB1OdWIu", + "citecheck__get_completed_incomplete.json": "gASVXQoAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMCmluY29tcGxldGWUjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lIwbMjAyNi0xMC0wMVQxMDowNzowMC4xMjM0NTZalIwScG9sbF9hZnRlcl9zZWNvbmRzlE6MBnBvbGljeZRoAIwPQ2l0ZWNoZWNrUG9saWN5lJOUKYGUfZQoaAV9lIwNbWF4X2NpdGF0aW9uc5RLAnOMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBiQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMEENpdGVjaGVja1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2l0YXRpb25zX2ZvdW5klEsCjBJjaXRhdGlvbnNfc2VsZWN0ZWSUSwKMDmNpdGF0aW9uX2xpbWl0lEsCjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklImMD2NpdGF0aW9uX2NoZWNrc5RoAIwZUmV2aWV3Q2l0YXRpb25DaGVja0NvdW50c5STlCmBlH2UKGgFfZQojAdjaGVja2VklEsBjAl1bmNoZWNrZWSUSwCMBmZhaWxlZJRLAXVoGX2UaBuPlChoLmgvaDCQaB1OdWKMD2NpdGF0aW9uX2lzc3Vlc5RLAHVoGX2UaBuPlChoJWgnaDNoJmgoaCSQaB1OdWKMB2NyZWRpdHOUaACMDVJldmlld0NyZWRpdHOUk5QpgZR9lChoBX2UjAdjaGFyZ2VklEsBc2gZfZRoG4+UKGg8kGgdTnVijAljaXRhdGlvbnOUXZQoaACMDlJldmlld0NpdGF0aW9ulJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAjAlyZWZlcmVuY2WUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwJY2l0ZWRfdXJslIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMA2RvaZROjAlzdGF0ZW1lbnSUjCJVbmVtcGxveW1lbnQgZmVsbCB0byA0LjElIGluIDIwMjQulIwGcXVvdGVzlF2UjAhwb3NpdGlvbpROjAZyZXN1bHSUaACMFFJldmlld0NpdGF0aW9uUmVzdWx0lJOUKYGUfZQoaAV9lCiMB2ZpbmRpbmeUjAlzdXBwb3J0ZWSUjAZzb3VyY2WUjAdzdXBwb3J0lIwIaXNfaXNzdWWUiXVoGX2UaBuPlChoV2hbaFmQaB1OdWKMBWNoZWNrlGgAjBNSZXZpZXdDaXRhdGlvbkNoZWNrlJOUKYGUfZQoaAV9lChoCYwJY29tcGxldGVklIwJcGFnZV9yZWFklIwEZnVsbJSMCnBhZ2VfdGl0bGWUjCdQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjBNwYWdlX3B1Ymxpc2hlZF9kYXRllE6MDXBhZ2VfbGFuZ3VhZ2WUjAJlbpSMCnNvdXJjZV91cmyUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwOc291cmNlX3ZlcnNpb26UTowHc3VwcG9ydJSMCXN1cHBvcnRlZJSMB3NuaXBwZXSUjBNUaGUgc291cmNlIHNheXMgc28ulIwJcmF0aW9uYWxllIwTVGhlIHBhZ2Ugc3RhdGVzIGl0LpSMBXF1b3RllE6MDW1pc3NpbmdfcXVvdGWUTowOZG9pX3JlZ2lzdGVyZWSUTowIbWV0YWRhdGGUTowUbWV0YWRhdGFfZGlmZmVyZW5jZXOUXZSMCnJlZ2lzdGVyZWSUTowQdW5jaGVja2VkX3JlYXNvbpROjARoaW50lE6MB2ZhaWx1cmWUTnVoGX2UaBuPlChoaWh8aHNoamhsaH1od2h4aAlodmh5aHVoZ2h7aG9oZWhuaHFofpBoHU51YnVoGX2UaBuPlChoTGhRaEdoXmhGaEtoTmhJaFCQaB1OdWJoQimBlH2UKGgFfZQoaEZLAWhHjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRoSYwdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaEtOaEyMIlRoZSBhZ2VuY3kgc2FpZCBzbyBpbiBhIHN0YXRlbWVudC6UaE5dlGhQTmhRTmheaGApgZR9lChoBX2UKGgJjAZmYWlsZWSUaGVOaGdOaGlOaGpOaGxOaG5OaG9OaHFOaHNOaHVOaHZOaHdOaHhOaHldlGh7Tmh8Tmh9jC5UaGUgY2hlY2sgZmFpbGVkIG9uIG91ciBzaWRlLiBUcnkgYWdhaW4gbGF0ZXIulGh+aACMDEZhaWx1cmVCbG9ja5STlCmBlH2UKGgFfZQojA5mYWlsdXJlX3JlYXNvbpSMCGludGVybmFslIwNZmFpbHVyZV9jbGFzc5SMCGludGVybmFslIwJcmV0cnlhYmxllIlofYwuVGhlIGNoZWNrIGZhaWxlZCBvbiBvdXIgc2lkZS4gVHJ5IGFnYWluIGxhdGVyLpSMCGRvY3NfdXJslIwkaHR0cHM6Ly9sZW56LmlvL2RvY3MvZXJyb3JzI2ludGVybmFslHVoGX2UaBuPlChofWiVaJtol2iZkGgdTnVidWgZfZRoG4+UKGhpaHxoc2hqaGxofWh3aHhoCWh2aHlodWhnaHtob2hlaG5ocWh+kGgdTnVidWgZfZRoG4+UKGhMaFFoR2heaEZoS2hOaEloUJBoHU51YmVoM12UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlGgAjBVSZXZpZXdDaXRhdGlvbkZhaWx1cmWUk5QpgZR9lChoBX2UKIwOY2l0YXRpb25faW5kZXiUSwFoR4wdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaEmMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhLTmh+aJEpgZR9lChoBX2UKGiVjAhpbnRlcm5hbJRol4wIaW50ZXJuYWyUaJmJaH2MLlRoZSBjaGVjayBmYWlsZWQgb24gb3VyIHNpZGUuIFRyeSBhZ2FpbiBsYXRlci6UaJuMJGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnRlcm5hbJR1aBl9lGgbj5QoaH1olWibaJdomZBoHU51YnVoGX2UaBuPlChoR2hLaKtoSWh+kGgdTnViYYwObW9yZV9jaXRhdGlvbnOUXZRofk51aBl9lIwIbGFuZ3VhZ2WUjAJlbpRzaBuPlChoC2g2aA9oDWi8aAloEmg/aDNopGgRaAdoHmi5aH6QaB1OdWIu", + "citecheck__get_completed_issues_found.json": "gASVGgsAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMDGlzc3Vlc19mb3VuZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjBJwb2xsX2FmdGVyX3NlY29uZHOUTowGcG9saWN5lGgAjA9DaXRlY2hlY2tQb2xpY3mUk5QpgZR9lChoBX2UjA1tYXhfY2l0YXRpb25zlEsUc4wSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwQQ2l0ZWNoZWNrU3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSxSMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwKMCXVuY2hlY2tlZJRLAIwGZmFpbGVklEsAdWgZfZRoG4+UKGguaC9oMJBoHU51YowPY2l0YXRpb25faXNzdWVzlEsBdWgZfZRoG4+UKGglaCdoM2gmaChoJJBoHU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwJzaBl9lGgbj5QoaDyQaB1OdWKMCWNpdGF0aW9uc5RdlChoAIwOUmV2aWV3Q2l0YXRpb26Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMCXJlZmVyZW5jZZSMGmEgcmVwb3J0IGZyb20gdGhlIG1pbmlzdHJ5lIwJY2l0ZWRfdXJslIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMA2RvaZROjAlzdGF0ZW1lbnSUjEtVbmVtcGxveW1lbnQgZmVsbCB0byA0LjElIGluIDIwMjQsIGFjY29yZGluZyB0byBhIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeS6UjAZxdW90ZXOUXZSMCHBvc2l0aW9ulGgAjAhQb3NpdGlvbpSTlCmBlH2UKGgFfZQojAVzdGFydJRLAIwDZW5klEtujAR0ZXh0lE51aBl9lGgbj5QoaFZoV2hYkGgdTnVijAZyZXN1bHSUaACMFFJldmlld0NpdGF0aW9uUmVzdWx0lJOUKYGUfZQoaAV9lCiMB2ZpbmRpbmeUjAxjb250cmFkaWN0ZWSUjAZzb3VyY2WUjAdzdXBwb3J0lIwIaXNfaXNzdWWUiHVoGX2UaBuPlChoYWhlaGOQaB1OdWKMBWNoZWNrlGgAjBNSZXZpZXdDaXRhdGlvbkNoZWNrlJOUKYGUfZQoaAV9lChoCYwJY29tcGxldGVklIwJcGFnZV9yZWFklIwEZnVsbJSMCnBhZ2VfdGl0bGWUjCdQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjBNwYWdlX3B1Ymxpc2hlZF9kYXRllE6MDXBhZ2VfbGFuZ3VhZ2WUjAJlbpSMCnNvdXJjZV91cmyUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwOc291cmNlX3ZlcnNpb26UTowHc3VwcG9ydJSMDGNvbnRyYWRpY3RlZJSMB3NuaXBwZXSUjBNUaGUgc291cmNlIHNheXMgc28ulIwJcmF0aW9uYWxllIwTVGhlIHBhZ2Ugc3RhdGVzIGl0LpSMBXF1b3RllE6MDW1pc3NpbmdfcXVvdGWUTowOZG9pX3JlZ2lzdGVyZWSUTowIbWV0YWRhdGGUTowUbWV0YWRhdGFfZGlmZmVyZW5jZXOUXZSMCnJlZ2lzdGVyZWSUTowQdW5jaGVja2VkX3JlYXNvbpROjARoaW50lE6MB2ZhaWx1cmWUTnVoGX2UaBuPlChoc2iGaH1odGh2aIdogWiCaAlogGiDaH9ocWiFaHlob2h4aHtoiJBoHU51YnVoGX2UaBuPlChoTGhbaEdoaGhGaEtoTmhJaFCQaB1OdWJoQimBlH2UKGgFfZQoaEZLAWhHjAthIHN0YXRlbWVudJRoSYwdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaEtOaEyMIlRoZSBhZ2VuY3kgc2FpZCBzbyBpbiBhIHN0YXRlbWVudC6UaE5dlGhQaFIpgZR9lChoBX2UKGhWS29oV0uyaFhOdWgZfZRoG4+UKGhWaFdoWJBoHU51YmhbaF0pgZR9lChoBX2UKGhhjAlzdXBwb3J0ZWSUaGOMB3N1cHBvcnSUaGWJdWgZfZRoG4+UKGhhaGVoY5BoHU51YmhoaGopgZR9lChoBX2UKGgJjAljb21wbGV0ZWSUaG+MBGZ1bGyUaHGMJVBhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaHNOaHSMAmVulGh2jB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRoeE5oeYwJc3VwcG9ydGVklGh7jBNUaGUgc291cmNlIHNheXMgc28ulGh9jBNUaGUgcGFnZSBzdGF0ZXMgaXQulGh/TmiATmiBTmiCTmiDXZRohU5ohk5oh05oiE51aBl9lGgbj5QoaHNohmh9aHRodmiHaIFogmgJaIBog2h/aHFohWh5aG9oeGh7aIiQaB1OdWJ1aBl9lGgbj5QoaExoW2hHaGhoRmhLaE5oSWhQkGgdTnViZWgzXZRoAIwTUmV2aWV3Q2l0YXRpb25Jc3N1ZZSTlCmBlH2UKGgFfZQojA5jaXRhdGlvbl9pbmRleJRLAGhHjBphIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeZRoSYwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJRoS05oTIxLVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LCBhY2NvcmRpbmcgdG8gYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnkulGhOXZRoUGhSKYGUfZQoaAV9lChoVksAaFdLbmhYTnVoGX2UaBuPlChoVmhXaFiQaB1OdWJoYYwMY29udHJhZGljdGVklGhjjAdzdXBwb3J0lGh7jBNUaGUgc291cmNlIHNheXMgc28ulGh9jBNUaGUgcGFnZSBzdGF0ZXMgaXQulGiATmiDXZRocYwnUGFnZSBhdCBodHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lGiITnVoGX2UaBuPlChoTGiAaEdog2hxaEtotmh9aE5oY2hhaEloe2iIaFCQaB1OdWJhjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRoiE51aBl9lIwIbGFuZ3VhZ2WUjAJlbpRzaBuPlChoC2g2aA9oDWjNaAloEmg/aDNoyGgRaAdoHmjKaIiQaB1OdWIu", + "citecheck__get_completed_partly_supported.json": "gASV3wgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROjAZwb2xpY3mUaACMD0NpdGVjaGVja1BvbGljeZSTlCmBlH2UKGgFfZSMDW1heF9jaXRhdGlvbnOUSwJzjBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjBBDaXRlY2hlY2tTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NpdGF0aW9uc19mb3VuZJRLAowSY2l0YXRpb25zX3NlbGVjdGVklEsCjA5jaXRhdGlvbl9saW1pdJRLAowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJSJjA9jaXRhdGlvbl9jaGVja3OUaACMGVJldmlld0NpdGF0aW9uQ2hlY2tDb3VudHOUk5QpgZR9lChoBX2UKIwHY2hlY2tlZJRLAowJdW5jaGVja2VklEsAjAZmYWlsZWSUSwB1aBl9lGgbj5QoaC5oL2gwkGgdTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwB1aBl9lGgbj5QoaCVoJ2gzaCZoKGgkkGgdTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoGX2UaBuPlChoPJBoHU51YowJY2l0YXRpb25zlF2UKGgAjA5SZXZpZXdDaXRhdGlvbpSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwJcmVmZXJlbmNllIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIwiVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LpSMBnF1b3Rlc5RdlIwIcG9zaXRpb26UTowGcmVzdWx0lGgAjBRSZXZpZXdDaXRhdGlvblJlc3VsdJSTlCmBlH2UKGgFfZQojAdmaW5kaW5nlIwQcGFydGx5X3N1cHBvcnRlZJSMBnNvdXJjZZSMB3N1cHBvcnSUjAhpc19pc3N1ZZSJdWgZfZRoG4+UKGhXaFtoWZBoHU51YowFY2hlY2uUaACME1Jldmlld0NpdGF0aW9uQ2hlY2uUk5QpgZR9lChoBX2UKGgJjAljb21wbGV0ZWSUjAlwYWdlX3JlYWSUjARmdWxslIwKcGFnZV90aXRsZZSMJ1BhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZSMAmVulIwKc291cmNlX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjA5zb3VyY2VfdmVyc2lvbpROjAdzdXBwb3J0lIwQcGFydGx5X3N1cHBvcnRlZJSMB3NuaXBwZXSUjBNUaGUgc291cmNlIHNheXMgc28ulIwJcmF0aW9uYWxllIwTVGhlIHBhZ2Ugc3RhdGVzIGl0LpSMBXF1b3RllE6MDW1pc3NpbmdfcXVvdGWUTowOZG9pX3JlZ2lzdGVyZWSUTowIbWV0YWRhdGGUTowUbWV0YWRhdGFfZGlmZmVyZW5jZXOUXZSMCnJlZ2lzdGVyZWSUTowQdW5jaGVja2VkX3JlYXNvbpROjARoaW50lE6MB2ZhaWx1cmWUTnVoGX2UaBuPlChoaWh8aHNoamhsaH1od2h4aAlodmh5aHVoZ2h7aG9oZWhuaHFofpBoHU51YnVoGX2UaBuPlChoTGhRaEdoXmhGaEtoTmhJaFCQaB1OdWJoQimBlH2UKGgFfZQoaEZLAWhHjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRoSYwdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaEtOaEyMIlRoZSBhZ2VuY3kgc2FpZCBzbyBpbiBhIHN0YXRlbWVudC6UaE5dlGhQTmhRaFMpgZR9lChoBX2UKGhXjAlzdXBwb3J0ZWSUaFmMB3N1cHBvcnSUaFuJdWgZfZRoG4+UKGhXaFtoWZBoHU51YmheaGApgZR9lChoBX2UKGgJjAljb21wbGV0ZWSUaGWMBGZ1bGyUaGeMJVBhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaGlOaGqMAmVulGhsjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRobk5ob4wJc3VwcG9ydGVklGhxjBNUaGUgc291cmNlIHNheXMgc28ulGhzjBNUaGUgcGFnZSBzdGF0ZXMgaXQulGh1Tmh2Tmh3Tmh4Tmh5XZRoe05ofE5ofU5ofk51aBl9lGgbj5QoaGlofGhzaGpobGh9aHdoeGgJaHZoeWh1aGdoe2hvaGVobmhxaH6QaB1OdWJ1aBl9lGgbj5QoaExoUWhHaF5oRmhLaE5oSWhQkGgdTnViZWgzXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjA5tb3JlX2NpdGF0aW9uc5RdlGh+TnVoGX2UjAhsYW5ndWFnZZSMAmVulHNoG4+UKGgLaDZoD2gNaKdoCWgSaD9oM2iiaBFoB2geaKRofpBoHU51Yi4=", + "citecheck__get_completed_unchecked.json": "gASVqwgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMCXVuY2hlY2tlZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjBJwb2xsX2FmdGVyX3NlY29uZHOUTowGcG9saWN5lGgAjA9DaXRlY2hlY2tQb2xpY3mUk5QpgZR9lChoBX2UjA1tYXhfY2l0YXRpb25zlEsCc4wSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwQQ2l0ZWNoZWNrU3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSwKMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwCMCXVuY2hlY2tlZJRLAowGZmFpbGVklEsAdWgZfZRoG4+UKGguaC9oMJBoHU51YowPY2l0YXRpb25faXNzdWVzlEsAdWgZfZRoG4+UKGglaCdoM2gmaChoJJBoHU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwBzaBl9lGgbj5QoaDyQaB1OdWKMCWNpdGF0aW9uc5RdlChoAIwOUmV2aWV3Q2l0YXRpb26Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMCXJlZmVyZW5jZZSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjAljaXRlZF91cmyUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwDZG9plE6MCXN0YXRlbWVudJSMIlVuZW1wbG95bWVudCBmZWxsIHRvIDQuMSUgaW4gMjAyNC6UjAZxdW90ZXOUXZSMCHBvc2l0aW9ulE6MBnJlc3VsdJRoAIwUUmV2aWV3Q2l0YXRpb25SZXN1bHSUk5QpgZR9lChoBX2UKIwHZmluZGluZ5SMCXVuY2hlY2tlZJSMBnNvdXJjZZROjAhpc19pc3N1ZZSJdWgZfZRoG4+UKGhXaFpoWZBoHU51YowFY2hlY2uUaACME1Jldmlld0NpdGF0aW9uQ2hlY2uUk5QpgZR9lChoBX2UKGgJjAljb21wbGV0ZWSUjAlwYWdlX3JlYWSUjARmdWxslIwKcGFnZV90aXRsZZSMJ1BhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZSMAmVulIwKc291cmNlX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjA5zb3VyY2VfdmVyc2lvbpROjAdzdXBwb3J0lIwJdW5jaGVja2VklIwHc25pcHBldJROjAlyYXRpb25hbGWUjBNUaGUgcGFnZSBzdGF0ZXMgaXQulIwFcXVvdGWUTowNbWlzc2luZ19xdW90ZZROjA5kb2lfcmVnaXN0ZXJlZJROjAhtZXRhZGF0YZROjBRtZXRhZGF0YV9kaWZmZXJlbmNlc5RdlIwKcmVnaXN0ZXJlZJROjBB1bmNoZWNrZWRfcmVhc29ulIwHbm9fdGV4dJSMBGhpbnSUTowHZmFpbHVyZZROdWgZfZRoG4+UKGhoaHpocWhpaGtofGh1aHZoCWh0aHdoc2hmaHlobmhkaG1ocGh9kGgdTnVidWgZfZRoG4+UKGhMaFFoR2hdaEZoS2hOaEloUJBoHU51YmhCKYGUfZQoaAV9lChoRksBaEeMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhJjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRoS05oTIwiVGhlIGFnZW5jeSBzYWlkIHNvIGluIGEgc3RhdGVtZW50LpRoTl2UaFBOaFFoUymBlH2UKGgFfZQoaFeMCXVuY2hlY2tlZJRoWU5oWol1aBl9lGgbj5QoaFdoWmhZkGgdTnViaF1oXymBlH2UKGgFfZQoaAmMCWNvbXBsZXRlZJRoZIwEZnVsbJRoZowlUGFnZSBhdCBodHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRoaE5oaYwCZW6UaGuMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhtTmhujAl1bmNoZWNrZWSUaHBOaHGME1RoZSBwYWdlIHN0YXRlcyBpdC6UaHNOaHROaHVOaHZOaHddlGh5Tmh6jAdub190ZXh0lGh8Tmh9TnVoGX2UaBuPlChoaGh6aHFoaWhraHxodWh2aAlodGh3aHNoZmh5aG5oZGhtaHBofZBoHU51YnVoGX2UaBuPlChoTGhRaEdoXWhGaEtoTmhJaFCQaB1OdWJlaDNdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaH1OdWgZfZSMCGxhbmd1YWdllIwCZW6Uc2gbj5QoaAtoNmgPaA1opWgJaBJoP2gzaKBoEWgHaB5oomh9kGgdTnViLg==", + "citecheck__get_failed_no_citations.json": "gASVFAQAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwGZmFpbGVklIwHb3V0Y29tZZSMCXVuY2hlY2tlZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjBJwb2xsX2FmdGVyX3NlY29uZHOUTowGcG9saWN5lGgAjA9DaXRlY2hlY2tQb2xpY3mUk5QpgZR9lChoBX2UjA1tYXhfY2l0YXRpb25zlEsUc4wSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwQQ2l0ZWNoZWNrU3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lEsUjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwB1aBl9lGgbj5QoaCVoJ2gpaCZoKGgkkGgdTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAHNoGX2UaBuPlChoMpBoHU51YowJY2l0YXRpb25zlF2UaCldlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMDm1vcmVfY2l0YXRpb25zlE6MB2ZhaWx1cmWUaACMDEZhaWx1cmVCbG9ja5STlCmBlH2UKGgFfZQojA5mYWlsdXJlX3JlYXNvbpSMDG5vX2NpdGF0aW9uc5SMDWZhaWx1cmVfY2xhc3OUjA1pbnZhbGlkX2lucHV0lIwJcmV0cnlhYmxllImMBGhpbnSUjE9UaGUgdGV4dCBoYXMgbm8gY2l0YXRpb24gdG8gY2hlY2s6IG5vIGxpbmssIERPSSBvciBudW1iZXJlZCByZWZlcmVuY2Ugd2l0aCBvbmUulIwIZG9jc191cmyUjClodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjaW52YWxpZC1pbnB1dJR1aBl9lGgbj5QoaEZoQWhIaENoRZBoHU51YnVoGX2UjAhsYW5ndWFnZZSMAmVulHNoG4+UKGgLaCxoD2gNaE1oCWgSaDVoKWg4aBFoB2geaDpoO5BoHU51Yi4=", + "citecheck__get_failed_upstream_unavailable.json": "gASVQAwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwGZmFpbGVklIwHb3V0Y29tZZSMCXVuY2hlY2tlZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjBJwb2xsX2FmdGVyX3NlY29uZHOUTowGcG9saWN5lGgAjA9DaXRlY2hlY2tQb2xpY3mUk5QpgZR9lChoBX2UjA1tYXhfY2l0YXRpb25zlEsUc4wSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwQQ2l0ZWNoZWNrU3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSxSMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwCMCXVuY2hlY2tlZJRLAIwGZmFpbGVklEsCdWgZfZRoG4+UKGguaC9oMJBoHU51YowPY2l0YXRpb25faXNzdWVzlEsAdWgZfZRoG4+UKGglaCdoM2gmaChoJJBoHU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwBzaBl9lGgbj5QoaDyQaB1OdWKMCWNpdGF0aW9uc5RdlChoAIwOUmV2aWV3Q2l0YXRpb26Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMCXJlZmVyZW5jZZSMGmEgcmVwb3J0IGZyb20gdGhlIG1pbmlzdHJ5lIwJY2l0ZWRfdXJslIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMA2RvaZROjAlzdGF0ZW1lbnSUTowGcXVvdGVzlF2UjAhwb3NpdGlvbpROjAZyZXN1bHSUTowFY2hlY2uUaACME1Jldmlld0NpdGF0aW9uQ2hlY2uUk5QpgZR9lChoBX2UKGgJjAZmYWlsZWSUjAlwYWdlX3JlYWSUTowKcGFnZV90aXRsZZROjBNwYWdlX3B1Ymxpc2hlZF9kYXRllE6MDXBhZ2VfbGFuZ3VhZ2WUTowKc291cmNlX3VybJROjA5zb3VyY2VfdmVyc2lvbpROjAdzdXBwb3J0lE6MB3NuaXBwZXSUTowJcmF0aW9uYWxllE6MBXF1b3RllE6MDW1pc3NpbmdfcXVvdGWUTowOZG9pX3JlZ2lzdGVyZWSUTowIbWV0YWRhdGGUTowUbWV0YWRhdGFfZGlmZmVyZW5jZXOUXZSMCnJlZ2lzdGVyZWSUTowQdW5jaGVja2VkX3JlYXNvbpROjARoaW50lIw7TGVueiBjb3VsZCBub3QgcmVhY2ggdGhpcyBzb3VyY2UganVzdCBub3cuIFRyeSBhZ2FpbiBsYXRlci6UjAdmYWlsdXJllGgAjAxGYWlsdXJlQmxvY2uUk5QpgZR9lChoBX2UKIwOZmFpbHVyZV9yZWFzb26UjBR1cHN0cmVhbV91bmF2YWlsYWJsZZSMDWZhaWx1cmVfY2xhc3OUjBR1cHN0cmVhbV91bmF2YWlsYWJsZZSMCXJldHJ5YWJsZZSIaGmMO0xlbnogY291bGQgbm90IHJlYWNoIHRoaXMgc291cmNlIGp1c3Qgbm93LiBUcnkgYWdhaW4gbGF0ZXIulIwIZG9jc191cmyUjDBodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjdXBzdHJlYW0tdW5hdmFpbGFibGWUdWgZfZRoG4+UKGhpaHFod2hzaHWQaB1OdWJ1aBl9lGgbj5QoaFpoaGhgaFtoXGhpaGNoZGgJaGJoZWhhaFloZ2heaFhoXWhfaGuQaB1OdWJ1aBl9lGgbj5QoaExoUGhHaFFoRmhLaE1oSWhPkGgdTnViaEIpgZR9lChoBX2UKGhGSwFoR4wLYSBzdGF0ZW1lbnSUaEmMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhLTmhMTmhNXZRoT05oUE5oUWhTKYGUfZQoaAV9lChoCYwGZmFpbGVklGhYTmhZTmhaTmhbTmhcTmhdTmheTmhfTmhgTmhhTmhiTmhjTmhkTmhlXZRoZ05oaE5oaYw7TGVueiBjb3VsZCBub3QgcmVhY2ggdGhpcyBzb3VyY2UganVzdCBub3cuIFRyeSBhZ2FpbiBsYXRlci6UaGtobSmBlH2UKGgFfZQoaHGMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGhzjBR1cHN0cmVhbV91bmF2YWlsYWJsZZRodYhoaYw7TGVueiBjb3VsZCBub3QgcmVhY2ggdGhpcyBzb3VyY2UganVzdCBub3cuIFRyeSBhZ2FpbiBsYXRlci6UaHeMMGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyN1cHN0cmVhbS11bmF2YWlsYWJsZZR1aBl9lGgbj5QoaGlocWh3aHNodZBoHU51YnVoGX2UaBuPlChoWmhoaGBoW2hcaGloY2hkaAloYmhlaGFoWWhnaF5oWGhdaF9oa5BoHU51YnVoGX2UaBuPlChoTGhQaEdoUWhGaEtoTWhJaE+QaB1OdWJlaDNdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZQoaACMFVJldmlld0NpdGF0aW9uRmFpbHVyZZSTlCmBlH2UKGgFfZQojA5jaXRhdGlvbl9pbmRleJRLAGhHjBphIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeZRoSYwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJRoS05oa2htKYGUfZQoaAV9lChocYwUdXBzdHJlYW1fdW5hdmFpbGFibGWUaHOMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGh1iGhpjDtMZW56IGNvdWxkIG5vdCByZWFjaCB0aGlzIHNvdXJjZSBqdXN0IG5vdy4gVHJ5IGFnYWluIGxhdGVyLpRod4wwaHR0cHM6Ly9sZW56LmlvL2RvY3MvZXJyb3JzI3Vwc3RyZWFtLXVuYXZhaWxhYmxllHVoGX2UaBuPlChoaWhxaHdoc2h1kGgdTnVidWgZfZRoG4+UKGhHaEtooGhJaGuQaB1OdWJonCmBlH2UKGgFfZQoaKBLAWhHjAthIHN0YXRlbWVudJRoSYwdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaEtOaGtobSmBlH2UKGgFfZQoaHGMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGhzjBR1cHN0cmVhbV91bmF2YWlsYWJsZZRodYhoaYw7TGVueiBjb3VsZCBub3QgcmVhY2ggdGhpcyBzb3VyY2UganVzdCBub3cuIFRyeSBhZ2FpbiBsYXRlci6UaHeMMGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyN1cHN0cmVhbS11bmF2YWlsYWJsZZR1aBl9lGgbj5QoaGlocWh3aHNodZBoHU51YnVoGX2UaBuPlChoR2hLaKBoSWhrkGgdTnViZYwObW9yZV9jaXRhdGlvbnOUXZRoa2htKYGUfZQoaAV9lChocYwUdXBzdHJlYW1fdW5hdmFpbGFibGWUaHOMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGh1iGhpTmh3jDBodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjdXBzdHJlYW0tdW5hdmFpbGFibGWUdWgZfZRoG4+UKGhpaHFod2hzaHWQaB1OdWJ1aBl9lIwIbGFuZ3VhZ2WUjAJlbpRzaBuPlChoC2g2aA9oDWjJaAloEmg/aDNomWgRaAdoHmi+aGuQaB1OdWIu", + "citecheck__get_queued.json": "gASVtgUAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwGcXVldWVklIwHb3V0Y29tZZROjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5RLCowGcG9saWN5lGgAjA9DaXRlY2hlY2tQb2xpY3mUk5QpgZR9lChoBX2UjA1tYXhfY2l0YXRpb25zlEsBc4wSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoFpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwQQ2l0ZWNoZWNrU3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jaXRhdGlvbnNfZm91bmSUSwGMEmNpdGF0aW9uc19zZWxlY3RlZJRLAYwOY2l0YXRpb25fbGltaXSUSwGMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwCMCXVuY2hlY2tlZJRLAIwGZmFpbGVklEsAdWgXfZRoGY+UKGgsaC1oLpBoG051YowPY2l0YXRpb25faXNzdWVzlEsAdWgXfZRoGY+UKGgjaCVoMWgkaCZoIpBoG051YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwBzaBd9lGgZj5QoaDqQaBtOdWKMCWNpdGF0aW9uc5RdlGgAjA5SZXZpZXdDaXRhdGlvbpSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwJcmVmZXJlbmNllIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIwiVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LpSMBnF1b3Rlc5RdlIwIcG9zaXRpb26UTowGcmVzdWx0lE6MBWNoZWNrlGgAjBNSZXZpZXdDaXRhdGlvbkNoZWNrlJOUKYGUfZQoaAV9lChoCYwHcGVuZGluZ5SMCXBhZ2VfcmVhZJROjApwYWdlX3RpdGxllE6ME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZROjApzb3VyY2VfdXJslE6MDnNvdXJjZV92ZXJzaW9ulE6MB3N1cHBvcnSUTowHc25pcHBldJROjAlyYXRpb25hbGWUTowFcXVvdGWUTowNbWlzc2luZ19xdW90ZZROjA5kb2lfcmVnaXN0ZXJlZJROjAhtZXRhZGF0YZROjBRtZXRhZGF0YV9kaWZmZXJlbmNlc5RdlIwKcmVnaXN0ZXJlZJROjBB1bmNoZWNrZWRfcmVhc29ulE6MBGhpbnSUTowHZmFpbHVyZZROdWgXfZRoGY+UKGhZaGdoX2haaFtoaGhiaGNoCWhhaGRoYGhYaGZoXWhXaFxoXmhpkGgbTnVidWgXfZRoGY+UKGhKaE9oRWhQaERoSWhMaEdoTpBoG051YmFoMV2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRoaU51aBd9lIwIbGFuZ3VhZ2WUjAJlbpRzaBmPlChoC2g0aA5oDGh0aAloEGg9aDFob2gPaAdoHGhxaGmQaBtOdWIu", + "citecheck__get_text_limit_exactly_at_limit.json": "gASVWQkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROjAZwb2xpY3mUaACMD0NpdGVjaGVja1BvbGljeZSTlCmBlH2UKGgFfZSMDW1heF9jaXRhdGlvbnOUSwJzjBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjBBDaXRlY2hlY2tTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NpdGF0aW9uc19mb3VuZJRLAowSY2l0YXRpb25zX3NlbGVjdGVklEsCjA5jaXRhdGlvbl9saW1pdJRLAowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJSJjA9jaXRhdGlvbl9jaGVja3OUaACMGVJldmlld0NpdGF0aW9uQ2hlY2tDb3VudHOUk5QpgZR9lChoBX2UKIwHY2hlY2tlZJRLAowJdW5jaGVja2VklEsAjAZmYWlsZWSUSwB1aBl9lGgbj5QoaC5oL2gwkGgdTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwB1aBl9lGgbj5QoaCVoJ2gzaCZoKGgkkGgdTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoGX2UaBuPlChoPJBoHU51YowJY2l0YXRpb25zlF2UKGgAjA5SZXZpZXdDaXRhdGlvbpSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwJcmVmZXJlbmNllIwaYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnmUjAljaXRlZF91cmyUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwDZG9plE6MCXN0YXRlbWVudJSMS1VuZW1wbG95bWVudCBmZWxsIHRvIDQuMSUgaW4gMjAyNCwgYWNjb3JkaW5nIHRvIGEgcmVwb3J0IGZyb20gdGhlIG1pbmlzdHJ5LpSMBnF1b3Rlc5RdlIwIcG9zaXRpb26UaACMCFBvc2l0aW9ulJOUKYGUfZQoaAV9lCiMBXN0YXJ0lEsAjANlbmSUS26MBHRleHSUTnVoGX2UaBuPlChoVmhXaFiQaB1OdWKMBnJlc3VsdJRoAIwUUmV2aWV3Q2l0YXRpb25SZXN1bHSUk5QpgZR9lChoBX2UKIwHZmluZGluZ5SMCXN1cHBvcnRlZJSMBnNvdXJjZZSMB3N1cHBvcnSUjAhpc19pc3N1ZZSJdWgZfZRoG4+UKGhhaGVoY5BoHU51YowFY2hlY2uUaACME1Jldmlld0NpdGF0aW9uQ2hlY2uUk5QpgZR9lChoBX2UKGgJjAljb21wbGV0ZWSUjAlwYWdlX3JlYWSUjARmdWxslIwKcGFnZV90aXRsZZSMJ1BhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZSMAmVulIwKc291cmNlX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjA5zb3VyY2VfdmVyc2lvbpROjAdzdXBwb3J0lIwJc3VwcG9ydGVklIwHc25pcHBldJSME1RoZSBzb3VyY2Ugc2F5cyBzby6UjAlyYXRpb25hbGWUjBNUaGUgcGFnZSBzdGF0ZXMgaXQulIwFcXVvdGWUTowNbWlzc2luZ19xdW90ZZROjA5kb2lfcmVnaXN0ZXJlZJROjAhtZXRhZGF0YZROjBRtZXRhZGF0YV9kaWZmZXJlbmNlc5RdlIwKcmVnaXN0ZXJlZJROjBB1bmNoZWNrZWRfcmVhc29ulE6MBGhpbnSUTowHZmFpbHVyZZROdWgZfZRoG4+UKGhzaIZofWh0aHZoh2iBaIJoCWiAaINof2hxaIVoeWhvaHhoe2iIkGgdTnVidWgZfZRoG4+UKGhMaFtoR2hoaEZoS2hOaEloUJBoHU51YmhCKYGUfZQoaAV9lChoRksBaEeMC2Egc3RhdGVtZW50lGhJjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRoS05oTIwiVGhlIGFnZW5jeSBzYWlkIHNvIGluIGEgc3RhdGVtZW50LpRoTl2UaFBoUimBlH2UKGgFfZQoaFZLb2hXS7JoWE51aBl9lGgbj5QoaFZoV2hYkGgdTnViaFtoXSmBlH2UKGgFfZQoaGGMCXN1cHBvcnRlZJRoY4wHc3VwcG9ydJRoZYl1aBl9lGgbj5QoaGFoZWhjkGgdTnViaGhoaimBlH2UKGgFfZQoaAmMCWNvbXBsZXRlZJRob4wEZnVsbJRocYwlUGFnZSBhdCBodHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRoc05odIwCZW6UaHaMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGh4Tmh5jAlzdXBwb3J0ZWSUaHuME1RoZSBzb3VyY2Ugc2F5cyBzby6UaH2ME1RoZSBwYWdlIHN0YXRlcyBpdC6UaH9OaIBOaIFOaIJOaINdlGiFTmiGTmiHTmiITnVoGX2UaBuPlChoc2iGaH1odGh2aIdogWiCaAlogGiDaH9ocWiFaHlob2h4aHtoiJBoHU51YnVoGX2UaBuPlChoTGhbaEdoaGhGaEtoTmhJaFCQaB1OdWJlaDNdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaIhOdWgZfZSMCGxhbmd1YWdllIwCZW6Uc2gbj5QoaAtoNmgPaA1otmgJaBJoP2gzaLFoEWgHaB5os2iIkGgdTnViLg==", + "citecheck__get_text_limit_reached.json": "gASVHggAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROjAZwb2xpY3mUaACMD0NpdGVjaGVja1BvbGljeZSTlCmBlH2UKGgFfZSMDW1heF9jaXRhdGlvbnOUSwFzjBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjBBDaXRlY2hlY2tTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NpdGF0aW9uc19mb3VuZJRLAowSY2l0YXRpb25zX3NlbGVjdGVklEsBjA5jaXRhdGlvbl9saW1pdJRLAYwWY2l0YXRpb25fbGltaXRfcmVhY2hlZJSIjA9jaXRhdGlvbl9jaGVja3OUaACMGVJldmlld0NpdGF0aW9uQ2hlY2tDb3VudHOUk5QpgZR9lChoBX2UKIwHY2hlY2tlZJRLAYwJdW5jaGVja2VklEsAjAZmYWlsZWSUSwB1aBl9lGgbj5QoaC5oL2gwkGgdTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwB1aBl9lGgbj5QoaCVoJ2gzaCZoKGgkkGgdTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAXNoGX2UaBuPlChoPJBoHU51YowJY2l0YXRpb25zlF2UaACMDlJldmlld0NpdGF0aW9ulJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAjAlyZWZlcmVuY2WUjBphIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeZSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIxLVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LCBhY2NvcmRpbmcgdG8gYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnkulIwGcXVvdGVzlF2UjAhwb3NpdGlvbpRoAIwIUG9zaXRpb26Uk5QpgZR9lChoBX2UKIwFc3RhcnSUSwCMA2VuZJRLbowEdGV4dJROdWgZfZRoG4+UKGhWaFdoWJBoHU51YowGcmVzdWx0lGgAjBRSZXZpZXdDaXRhdGlvblJlc3VsdJSTlCmBlH2UKGgFfZQojAdmaW5kaW5nlIwJc3VwcG9ydGVklIwGc291cmNllIwHc3VwcG9ydJSMCGlzX2lzc3VllIl1aBl9lGgbj5QoaGFoZWhjkGgdTnVijAVjaGVja5RoAIwTUmV2aWV3Q2l0YXRpb25DaGVja5STlCmBlH2UKGgFfZQoaAmMCWNvbXBsZXRlZJSMCXBhZ2VfcmVhZJSMBGZ1bGyUjApwYWdlX3RpdGxllIwnUGFnZSBhdCBodHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwTcGFnZV9wdWJsaXNoZWRfZGF0ZZROjA1wYWdlX2xhbmd1YWdllIwCZW6UjApzb3VyY2VfdXJslIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMDnNvdXJjZV92ZXJzaW9ulE6MB3N1cHBvcnSUjAlzdXBwb3J0ZWSUjAdzbmlwcGV0lIwTVGhlIHNvdXJjZSBzYXlzIHNvLpSMCXJhdGlvbmFsZZSME1RoZSBwYWdlIHN0YXRlcyBpdC6UjAVxdW90ZZROjA1taXNzaW5nX3F1b3RllE6MDmRvaV9yZWdpc3RlcmVklE6MCG1ldGFkYXRhlE6MFG1ldGFkYXRhX2RpZmZlcmVuY2VzlF2UjApyZWdpc3RlcmVklE6MEHVuY2hlY2tlZF9yZWFzb26UTowEaGludJROjAdmYWlsdXJllE51aBl9lGgbj5QoaHNohmh9aHRodmiHaIFogmgJaIBog2h/aHFohWh5aG9oeGh7aIiQaB1OdWJ1aBl9lGgbj5QoaExoW2hHaGhoRmhLaE5oSWhQkGgdTnViYWgzXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjA5tb3JlX2NpdGF0aW9uc5RdlGgAjBJSZXZpZXdNb3JlQ2l0YXRpb26Uk5QpgZR9lChoBX2UKGhGSwFoR4wLYSBzdGF0ZW1lbnSUaEmMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhLTowIc2VudGVuY2WUjCJUaGUgYWdlbmN5IHNhaWQgc28gaW4gYSBzdGF0ZW1lbnQulGhQaFIpgZR9lChoBX2UKGhWS29oV0uyaFhOdWgZfZRoG4+UKGhWaFdoWJBoHU51YnVoGX2UaBuPlChoR2iZaEZoS2hJaFCQaB1OdWJhaIhOdWgZfZSMCGxhbmd1YWdllIwCZW6Uc2gbj5QoaAtoNmgPaA1oo2gJaBJoP2gzaI5oEWgHaB5okGiIkGgdTnViLg==", + "citecheck__idempotent_replay_202.json": "gASVuwAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQQ2l0ZWNoZWNrU3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAxjaXRlY2hlY2tfaWSUjAg4ZWI0OTMwMpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgHaAmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "citecheck__receipt_202.json": "gASVuwAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQQ2l0ZWNoZWNrU3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAxjaXRlY2hlY2tfaWSUjAg4ZWI0OTMwMpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgHaAmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "citecheck__receipt_202_empty_webhook_url.json": "gASVuwAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQQ2l0ZWNoZWNrU3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAxjaXRlY2hlY2tfaWSUjAg4ZWI0OTMwMpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgHaAmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "extract__focus_ready.json": "gASVMgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBdCZXRhIGZlbGwgMyUgbGFzdCB5ZWFyLpSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjBBjYW5kaWRhdGVfY2xhaW1zlF2UjAZkb21haW6UjAlFY29ub21pY3OUjAxrZXlfZW50aXRpZXOUXZQoaACMD0V4dHJhY3RlZEVudGl0eZSTlCmBlH2UKGgFfZQojARuYW1llIwFQWxwaGGUjAR0eXBllIwMb3JnYW5pemF0aW9ulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBhoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgUKYGUfZQoaAV9lChoGIwEQmV0YZRoGowMb3JnYW5pemF0aW9ulHVoHH2UaB6PlChoGGgakGggTnViZYwPcHJlc3VtZWRfaW50ZW50lIwXVmVyaWZ5IHJlcG9ydGVkIGZpZ3VyZXOUjA5vcmlnaW5hbF9pbnB1dJSMLkFscGhhIHJvc2UgNSUgaW4gMjAyNC4gQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UjAlsb2NhdGlvbnOUTnVoHH2UaB6PlChoEWgJaA1oC2gHaA9oLGgqaCiQaCBOdWIu", + "extract__idempotency_replay.json": "gASVZgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZQojBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwXQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UZYwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulIwJRWNvbm9taWNzlIwMa2V5X2VudGl0aWVzlF2UKGgAjA9FeHRyYWN0ZWRFbnRpdHmUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUFscGhhlIwEdHlwZZSMDG9yZ2FuaXphdGlvbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaByQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoFimBlH2UKGgFfZQoaBqMBEJldGGUaByMDG9yZ2FuaXphdGlvbpR1aB59lGggj5QoaBpoHJBoIk51YmWMD3ByZXN1bWVkX2ludGVudJSMF1ZlcmlmeSByZXBvcnRlZCBmaWd1cmVzlIwOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlE51aB59lGggj5QoaBNoCWgPaAtoB2gRaC5oLGgqkGgiTnViLg==", + "extract__locate_all_dropped.json": "gASVGAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMC25vdF9hX2NsYWltlIwFY2xhaW2UjACUjBFpZGVudGlmaWVkX2NsYWltc5RdlIwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulGgKjAxrZXlfZW50aXRpZXOUXZQoaACMD0V4dHJhY3RlZEVudGl0eZSTlCmBlH2UKGgFfZQojARuYW1llIwFQWxwaGGUjAR0eXBllIwMb3JnYW5pemF0aW9ulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBdoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgTKYGUfZQoaAV9lChoF4wEQmV0YZRoGYwMb3JnYW5pemF0aW9ulHVoG32UaB2PlChoF2gZkGgfTnViZYwPcHJlc3VtZWRfaW50ZW50lIwXVmVyaWZ5IHJlcG9ydGVkIGZpZ3VyZXOUjA5vcmlnaW5hbF9pbnB1dJSMLkFscGhhIHJvc2UgNSUgaW4gMjAyNC4gQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UjAlsb2NhdGlvbnOUXZR1aBt9lGgdj5QoaBBoCWgNaAtoB2gPaCtoKWgnkGgfTnViLg==", + "extract__locate_failed.json": "gASVZgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZQojBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwXQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UZYwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulIwJRWNvbm9taWNzlIwMa2V5X2VudGl0aWVzlF2UKGgAjA9FeHRyYWN0ZWRFbnRpdHmUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUFscGhhlIwEdHlwZZSMDG9yZ2FuaXphdGlvbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaByQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoFimBlH2UKGgFfZQoaBqMBEJldGGUaByMDG9yZ2FuaXphdGlvbpR1aB59lGggj5QoaBpoHJBoIk51YmWMD3ByZXN1bWVkX2ludGVudJSMF1ZlcmlmeSByZXBvcnRlZCBmaWd1cmVzlIwOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlE51aB59lGggj5QoaBNoCWgPaAtoB2gRaC5oLGgqkGgiTnViLg==", + "extract__locate_leading_whitespace.json": "gASV8gIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMEGNhbmRpZGF0ZV9jbGFpbXOUXZSMBmRvbWFpbpSMCUVjb25vbWljc5SMDGtleV9lbnRpdGllc5RdlChoAIwPRXh0cmFjdGVkRW50aXR5lJOUKYGUfZQoaAV9lCiMBG5hbWWUjAVBbHBoYZSMBHR5cGWUjAxvcmdhbml6YXRpb26UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaBQpgZR9lChoBX2UKGgYjARCZXRhlGgajAxvcmdhbml6YXRpb26UdWgcfZRoHo+UKGgYaBqQaCBOdWJljA9wcmVzdW1lZF9pbnRlbnSUjBdWZXJpZnkgcmVwb3J0ZWQgZmlndXJlc5SMDm9yaWdpbmFsX2lucHV0lIwuQWxwaGEgcm9zZSA1JSBpbiAyMDI0LiBCZXRhIGZlbGwgMyUgbGFzdCB5ZWFyLpSMCWxvY2F0aW9uc5RdlGgAjA1DbGFpbUxvY2F0aW9ulJOUKYGUfZQoaAV9lChoCYwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpSMCXBvc2l0aW9uc5RdlGgAjAhQb3NpdGlvbpSTlCmBlH2UKGgFfZQojAVzdGFydJRLA4wDZW5klEsZjAR0ZXh0lIwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpR1aBx9lGgej5QoaDtoPGg9kGggTnViYXVoHH2UaB6PlChoCWg0kGggTnViYXVoHH2UaB6PlChoEWgJaA1oC2gHaA9oLGgqaCiQaCBOdWIu", + "extract__locate_some_dropped.json": "gASVsQMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZQojBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwXQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UZYwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulIwJRWNvbm9taWNzlIwMa2V5X2VudGl0aWVzlF2UKGgAjA9FeHRyYWN0ZWRFbnRpdHmUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUFscGhhlIwEdHlwZZSMDG9yZ2FuaXphdGlvbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaByQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoFimBlH2UKGgFfZQoaBqMBEJldGGUaByMDG9yZ2FuaXphdGlvbpR1aB59lGggj5QoaBpoHJBoIk51YmWMD3ByZXN1bWVkX2ludGVudJSMF1ZlcmlmeSByZXBvcnRlZCBmaWd1cmVzlIwOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlF2UKGgAjA1DbGFpbUxvY2F0aW9ulJOUKYGUfZQoaAV9lChoCYwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpSMCXBvc2l0aW9uc5RdlGgAjAhQb3NpdGlvbpSTlCmBlH2UKGgFfZQojAVzdGFydJRLAIwDZW5klEsWjAR0ZXh0lIwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpR1aB59lGggj5QoaD1oPmg/kGgiTnViYXVoHn2UaCCPlChoCWg2kGgiTnViaDEpgZR9lChoBX2UKGgJjBdCZXRhIGZlbGwgMyUgbGFzdCB5ZWFyLpRoNl2UaDkpgZR9lChoBX2UKGg9SxdoPksuaD+MF0JldGEgZmVsbCAzJSBsYXN0IHllYXIulHVoHn2UaCCPlChoPWg+aD+QaCJOdWJhdWgefZRoII+UKGgJaDaQaCJOdWJldWgefZRoII+UKGgTaAloD2gLaAdoEWguaCxoKpBoIk51Yi4=", + "extract__locate_true.json": "gASVsQMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZQojBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwXQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UZYwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulIwJRWNvbm9taWNzlIwMa2V5X2VudGl0aWVzlF2UKGgAjA9FeHRyYWN0ZWRFbnRpdHmUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUFscGhhlIwEdHlwZZSMDG9yZ2FuaXphdGlvbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaByQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoFimBlH2UKGgFfZQoaBqMBEJldGGUaByMDG9yZ2FuaXphdGlvbpR1aB59lGggj5QoaBpoHJBoIk51YmWMD3ByZXN1bWVkX2ludGVudJSMF1ZlcmlmeSByZXBvcnRlZCBmaWd1cmVzlIwOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlF2UKGgAjA1DbGFpbUxvY2F0aW9ulJOUKYGUfZQoaAV9lChoCYwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpSMCXBvc2l0aW9uc5RdlGgAjAhQb3NpdGlvbpSTlCmBlH2UKGgFfZQojAVzdGFydJRLAIwDZW5klEsWjAR0ZXh0lIwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpR1aB59lGggj5QoaD1oPmg/kGgiTnViYXVoHn2UaCCPlChoCWg2kGgiTnViaDEpgZR9lChoBX2UKGgJjBdCZXRhIGZlbGwgMyUgbGFzdCB5ZWFyLpRoNl2UaDkpgZR9lChoBX2UKGg9SxdoPksuaD+MF0JldGEgZmVsbCAzJSBsYXN0IHllYXIulHVoHn2UaCCPlChoPWg+aD+QaCJOdWJhdWgefZRoII+UKGgJaDaQaCJOdWJldWgefZRoII+UKGgTaAloD2gLaAdoEWguaCxoKpBoIk51Yi4=", + "extract__no_match_with_focus.json": "gASVZQEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMCG5vX21hdGNolIwFY2xhaW2UjACUjBFpZGVudGlmaWVkX2NsYWltc5RdlIwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulGgKjAxrZXlfZW50aXRpZXOUXZSMD3ByZXN1bWVkX2ludGVudJRoCowOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgQaAloDWgLaAdoD2gVaBNoEpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "extract__not_a_claim.json": "gASVTAEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMC25vdF9hX2NsYWltlIwFY2xhaW2UjACUjBFpZGVudGlmaWVkX2NsYWltc5RdlIwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulGgKjAxrZXlfZW50aXRpZXOUXZSMD3ByZXN1bWVkX2ludGVudJRoCowOb3JpZ2luYWxfaW5wdXSUjBJXaGF0IGEgbG92ZWx5IGRheS6UjAlsb2NhdGlvbnOUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBBoCWgNaAtoB2gPaBVoE2gSkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViLg==", + "extract__not_a_claim_beside_claims.json": "gASVHwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMC25vdF9hX2NsYWltlIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMEGNhbmRpZGF0ZV9jbGFpbXOUXZSMBmRvbWFpbpSMCUVjb25vbWljc5SMDGtleV9lbnRpdGllc5RdlChoAIwPRXh0cmFjdGVkRW50aXR5lJOUKYGUfZQoaAV9lCiMBG5hbWWUjAVBbHBoYZSMBHR5cGWUjAxvcmdhbml6YXRpb26UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaBQpgZR9lChoBX2UKGgYjARCZXRhlGgajAxvcmdhbml6YXRpb26UdWgcfZRoHo+UKGgYaBqQaCBOdWJljA9wcmVzdW1lZF9pbnRlbnSUjBdWZXJpZnkgcmVwb3J0ZWQgZmlndXJlc5SMDm9yaWdpbmFsX2lucHV0lIwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpSMCWxvY2F0aW9uc5ROdWgcfZRoHo+UKGgRaAloDWgLaAdoD2gsaCpoKJBoIE51Yi4=", + "extract__ready_one_claim.json": "gASVGQIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMEGNhbmRpZGF0ZV9jbGFpbXOUXZSMBmRvbWFpbpSMCUVjb25vbWljc5SMDGtleV9lbnRpdGllc5RdlChoAIwPRXh0cmFjdGVkRW50aXR5lJOUKYGUfZQoaAV9lCiMBG5hbWWUjAVBbHBoYZSMBHR5cGWUjAxvcmdhbml6YXRpb26UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaBQpgZR9lChoBX2UKGgYjARCZXRhlGgajAxvcmdhbml6YXRpb26UdWgcfZRoHo+UKGgYaBqQaCBOdWJljA9wcmVzdW1lZF9pbnRlbnSUjBdWZXJpZnkgcmVwb3J0ZWQgZmlndXJlc5SMDm9yaWdpbmFsX2lucHV0lIwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpSMCWxvY2F0aW9uc5ROdWgcfZRoHo+UKGgRaAloDWgLaAdoD2gsaCpoKJBoIE51Yi4=", + "extract__ready_several_claims.json": "gASVZgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZQojBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwXQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UZYwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulIwJRWNvbm9taWNzlIwMa2V5X2VudGl0aWVzlF2UKGgAjA9FeHRyYWN0ZWRFbnRpdHmUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUFscGhhlIwEdHlwZZSMDG9yZ2FuaXphdGlvbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaByQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoFimBlH2UKGgFfZQoaBqMBEJldGGUaByMDG9yZ2FuaXphdGlvbpR1aB59lGggj5QoaBpoHJBoIk51YmWMD3ByZXN1bWVkX2ludGVudJSMF1ZlcmlmeSByZXBvcnRlZCBmaWd1cmVzlIwOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlE51aB59lGggj5QoaBNoCWgPaAtoB2gRaC5oLGgqkGgiTnViLg==", + "extract__replay_later_200.json": "gASVZgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZQojBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwXQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UZYwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulIwJRWNvbm9taWNzlIwMa2V5X2VudGl0aWVzlF2UKGgAjA9FeHRyYWN0ZWRFbnRpdHmUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUFscGhhlIwEdHlwZZSMDG9yZ2FuaXphdGlvbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaByQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoFimBlH2UKGgFfZQoaBqMBEJldGGUaByMDG9yZ2FuaXphdGlvbpR1aB59lGggj5QoaBpoHJBoIk51YmWMD3ByZXN1bWVkX2ludGVudJSMF1ZlcmlmeSByZXBvcnRlZCBmaWd1cmVzlIwOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlE51aB59lGggj5QoaBNoCWgPaAtoB2gRaC5oLGgqkGgiTnViLg==", + "extract__url_input_located.json": "gASV4AIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMEGNhbmRpZGF0ZV9jbGFpbXOUXZSMBmRvbWFpbpSMCUVjb25vbWljc5SMDGtleV9lbnRpdGllc5RdlChoAIwPRXh0cmFjdGVkRW50aXR5lJOUKYGUfZQoaAV9lCiMBG5hbWWUjAVBbHBoYZSMBHR5cGWUjAxvcmdhbml6YXRpb26UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaBQpgZR9lChoBX2UKGgYjARCZXRhlGgajAxvcmdhbml6YXRpb26UdWgcfZRoHo+UKGgYaBqQaCBOdWJljA9wcmVzdW1lZF9pbnRlbnSUjBdWZXJpZnkgcmVwb3J0ZWQgZmlndXJlc5SMDm9yaWdpbmFsX2lucHV0lIweaHR0cHM6Ly9leGFtcGxlLmNvbS9hbi1hcnRpY2xllIwJbG9jYXRpb25zlF2UaACMDUNsYWltTG9jYXRpb26Uk5QpgZR9lChoBX2UKGgJjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwJcG9zaXRpb25zlF2UaACMCFBvc2l0aW9ulJOUKYGUfZQoaAV9lCiMBXN0YXJ0lE6MA2VuZJROjAR0ZXh0lIwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpR1aBx9lGgej5QoaDtoPGg9kGggTnViYXVoHH2UaB6PlChoCWg0kGggTnViYXVoHH2UaB6PlChoEWgJaA1oC2gHaA9oLGgqaCiQaCBOdWIu", + "review__get_assessing.json": "gASVTgcAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJYXNzZXNzaW5nlIwHb3V0Y29tZZROjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lIwbMjAyNi0xMC0wMVQxMDowNzowMC4xMjM0NTZalIwIbGFuZ3VhZ2WUjAJlbpSMBnBvbGljeZRoAIwQRXNjYWxhdGlvblBvbGljeZSTlCmBlH2UKGgFfZQojAh2ZXJkaWN0c5RdlCiMBUZhbHNllIwMTW9zdGx5IEZhbHNllIwFTWl4ZWSUZYwKY29uZmlkZW5jZZRdlIwDbG93lGGMD21heF9hc3Nlc3NtZW50c5RLFIwRbWF4X3ZlcmlmaWNhdGlvbnOUSwWMBWRlcHRolIwIc3RhbmRhcmSUjA1tYXhfY2l0YXRpb25zlEsAjA1zdWdnZXN0X2VkaXRzlIl1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaCRoH2gmaCdoI2gikIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjA1SZXZpZXdTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NsYWltc19zZWxlY3RlZJRLAowLY2xhaW1fbGltaXSUSxSME2NsYWltX2xpbWl0X3JlYWNoZWSUiYwPaW5wdXRfdHJ1bmNhdGVklImMC2Fzc2Vzc21lbnRzlGgAjBZSZXZpZXdBc3Nlc3NtZW50Q291bnRzlJOUKYGUfZQoaAV9lCiMCWNvbXBsZXRlZJRLAIwGZmFpbGVklEsAdWgofZRoKo+UKGg9aD6QaCxOdWKMDXZlcmlmaWNhdGlvbnOUTowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKH2UaCqPlChoM2g3aDZoRGhBaEloRmhCaDRoRWhIaEdoQ2g1kGgsTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoKH2UaCqPlChoUpBoLE51YowScG9sbF9hZnRlcl9zZWNvbmRzlEsKaEJdlIwIZmFpbHVyZXOUXZRoSF2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlIwHZmFpbHVyZZROjAZjbGFpbXOUXZQoaACMC1Jldmlld0NsYWltlJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAjAVjbGFpbZSMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lE6MCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAdydW5uaW5nlIwHdmVyZGljdJROaB9OjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBB2ZXJpZmljYXRpb25fdXJslE6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTmhiTnVoKH2UaCqPlChofWh4aHloH2gLaHZoe2h3aHpofmhikGgsTnVijAplc2NhbGF0aW9ulE6MDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKH2UaCqPlChoa2hvaIFoamiCaINobmhtkGgsTnViaGYpgZR9lChoBX2UKGhqSwFoa4wiV2F0ZXIgYm9pbHMgYXQgMTAwIEMgYXQgc2VhIGxldmVsLpRobU5obk5ob2hxKYGUfZQoaAV9lChoC4wHcnVubmluZ5Rodk5oH05od05oeE5oeU5oek5oe12UaH1OaH5OaGJOdWgofZRoKo+UKGh9aHhoeWgfaAtodmh7aHdoemh+aGKQaCxOdWJogU5ogk5og051aCh9lGgqj5QoaGtob2iBaGpogmiDaG5obZBoLE51YmWMCWNpdGF0aW9uc5RdlHVoKH2UaCqPlChoXGhXaBBoC2gUaFpoVWgtaGBoTGgOaEhoY2gNaF5oEmgJaJNoQmgHaGKQaCxOdWIu", + "review__get_assessment_rows_full_fields.json": "gASV+QwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMCmluY29tcGxldGWUjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lIwbMjAyNi0xMC0wMVQxMDowNzowMC4xMjM0NTZalIwIbGFuZ3VhZ2WUjAJlbpSMBnBvbGljeZRoAIwQRXNjYWxhdGlvblBvbGljeZSTlCmBlH2UKGgFfZQojAh2ZXJkaWN0c5RdlCiMBUZhbHNllIwMTW9zdGx5IEZhbHNllIwFTWl4ZWSUZYwKY29uZmlkZW5jZZRdlIwDbG93lGGMD21heF9hc3Nlc3NtZW50c5RLFIwRbWF4X3ZlcmlmaWNhdGlvbnOUSwCMBWRlcHRolIwIc3RhbmRhcmSUjA1tYXhfY2l0YXRpb25zlEsAjA1zdWdnZXN0X2VkaXRzlIl1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgbaCVoIGgnaChoJGgjkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjA1SZXZpZXdTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NsYWltc19zZWxlY3RlZJRLAowLY2xhaW1fbGltaXSUSxSME2NsYWltX2xpbWl0X3JlYWNoZWSUiYwPaW5wdXRfdHJ1bmNhdGVklImMC2Fzc2Vzc21lbnRzlGgAjBZSZXZpZXdBc3Nlc3NtZW50Q291bnRzlJOUKYGUfZQoaAV9lCiMCWNvbXBsZXRlZJRLAYwGZmFpbGVklEsBdWgpfZRoK4+UKGg+aD+QaC1OdWKMDXZlcmlmaWNhdGlvbnOUaACMGFJldmlld1ZlcmlmaWNhdGlvbkNvdW50c5STlCmBlH2UKGgFfZQojAdwbGFubmVklEsAaD5LAGg/SwB1aCl9lGgrj5QoaD5oP2hIkGgtTnVijAZpc3N1ZXOUSwGMD2NpdGF0aW9uc19mb3VuZJROjBJjaXRhdGlvbnNfc2VsZWN0ZWSUTowOY2l0YXRpb25fbGltaXSUTowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJROjA9jaXRhdGlvbl9jaGVja3OUTowPY2l0YXRpb25faXNzdWVzlEsAjBFjaXRhdGlvbnNfc2tpcHBlZJROdWgpfZRoK4+UKGg0aDhoN2hNaEJoUmhPaEtoNWhOaFFoUGhMaDaQaC1OdWKMB2NyZWRpdHOUaACMDVJldmlld0NyZWRpdHOUk5QpgZR9lChoBX2UjAdjaGFyZ2VklEsCc2gpfZRoK4+UKGhbkGgtTnVijBJwb2xsX2FmdGVyX3NlY29uZHOUTmhLXZRoAIwLUmV2aWV3SXNzdWWUk5QpgZR9lChoBX2UKIwLY2xhaW1faW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMDnZlcmlmaWVkX2NsYWltlE6MB3ZlcmRpY3SUjAVGYWxzZZRoIIwEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjA92ZXJpZmljYXRpb25faWSUTowTdmVyaWZpY2F0aW9uX3N0YXR1c5ROjBB2ZXJpZmljYXRpb25fdXJslE6MA3VybJROjAplc2NhbGF0aW9ulGgAjApFc2NhbGF0aW9ulJOUKYGUfZQoaAV9lCiMDW1hdGNoZWRfcnVsZXOUXZSMB3ZlcmRpY3SUYYwLZGlzcG9zaXRpb26UjANjYXCUdWgpfZRoK4+UKGh7aHiQaC1OdWKMC2tleV9maW5kaW5nlE6MCXJhdGlvbmFsZZSMI1R3byByZXZpZXdlcnMgZm91bmQgdGhlIGRhdGUgd3JvbmculIwRc3VnZ2VzdGVkX3Jld3JpdGWUjDBUaGUgRVUgQUkgQWN0IGVudGVyZWQgaW50byBmb3JjZSBpbiBBdWd1c3QgMjAyNC6UjAdmYWlsdXJllE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGhmaG5ocGggaGlob2hyaIBohWhxaGxoaGhlaH9ogmiEkGgtTnViYYwIZmFpbHVyZXOUXZRoAIwNUmV2aWV3RmFpbHVyZZSTlCmBlH2UKGgFfZQoaGVLAWhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjAVzdGFnZZSMCmFzc2Vzc21lbnSUaIRoAIwMRmFpbHVyZUJsb2NrlJOUKYGUfZQoaAV9lCiMDmZhaWx1cmVfcmVhc29ulIwHdGltZW91dJSMDWZhaWx1cmVfY2xhc3OUjBR1cHN0cmVhbV91bmF2YWlsYWJsZZSMCXJldHJ5YWJsZZSIjARoaW50lIw5VGhlIGNoZWNrIHJhbiBvdXQgb2YgdGltZS4gUmV0cnkgaXQ7IG5vdGhpbmcgd2FzIGNoYXJnZWQulIwIZG9jc191cmyUTnVoKX2UaCuPlChonGiXaJ5omWibkGgtTnVidWgpfZRoK4+UKGhmaGVokGiEkGgtTnViYWhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaIROjAZjbGFpbXOUXZQoaACMC1Jldmlld0NsYWltlJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAaGaMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKGhpjAVGYWxzZZRoIIwEaGlnaJRobIwKYXNzZXNzbWVudJSMCGlzX2lzc3VllIh1aCl9lGgrj5QoaCBoaWhsaL+QaC1OdWKMCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaGmMBUZhbHNllGggjARoaWdolGiAjCNUd28gcmV2aWV3ZXJzIGZvdW5kIHRoZSBkYXRlIHdyb25nLpSMB2Rpc3NlbnSUjDpPbmUgcmV2aWV3ZXIgcmVhZCB0aGUgY2xhaW0gYXMgYWJvdXQgdGhlIGVudHJ5IGludG8gZm9yY2UulGhwTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlCiMIlRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gMjAyNC6UjBhJdCB0b29rIGVmZmVjdCBpbiBNYXJjaC6UZWicjCRUaGlzIHRleHQgaG9sZHMgbW9yZSB0aGFuIG9uZSBjbGFpbS6UaIKMMFRoZSBFVSBBSSBBY3QgZW50ZXJlZCBpbnRvIGZvcmNlIGluIEF1Z3VzdCAyMDI0LpRohE51aCl9lGgrj5QoaJxozGhwaCBoC2hpaM9ogGjOaIJohJBoLU51YmhyaHQpgZR9lChoBX2UKGh4XZSMB3ZlcmRpY3SUYWh7jANjYXCUdWgpfZRoK4+UKGh7aHiQaC1OdWKMDHZlcmlmaWNhdGlvbpROaIVOdWgpfZRoK4+UKGhmaMJocmizaN9ohWi2aLWQaC1OdWJorymBlH2UKGgFfZQoaLNLAWhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UaLVOaLZOaMJoxCmBlH2UKGgFfZQoaAuMBmZhaWxlZJRoaU5oIE5ogE5ozE5ocE5ozowHdGltZW91dJRoz12UaJxOaIJOaIRokymBlH2UKGgFfZQoaJeMB3RpbWVvdXSUaJmMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGibiGicjDlUaGUgY2hlY2sgcmFuIG91dCBvZiB0aW1lLiBSZXRyeSBpdDsgbm90aGluZyB3YXMgY2hhcmdlZC6UaJ5OdWgpfZRoK4+UKGicaJdonmiZaJuQaC1OdWJ1aCl9lGgrj5QoaJxozGhwaCBoC2hpaM9ogGjOaIJohJBoLU51YmhyTmjfTmiFTnVoKX2UaCuPlChoZmjCaHJos2jfaIVotmi1kGgtTnViZYwJY2l0YXRpb25zlF2UdWgpfZRoK4+UKGimaIhoEWgLaBVopGheaC5oqmhVaA9oUWisaA1oqGgTaAlo+GhLaAdohJBoLU51Yi4=", + "review__get_citations_clean.json": "gASV2w0AAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLFIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSxSMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwKMCXVuY2hlY2tlZJRLAGg/SwB1aCl9lGgrj5QoaFZoV2g/kGgtTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhbaE9oS2g1aE5oWmhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwNzaCl9lGgrj5QoaGSQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlIwIZmFpbHVyZXOUXZRoWl2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlIwHZmFpbHVyZZROjAZjbGFpbXOUXZRoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQojAd2ZXJkaWN0lIwEVHJ1ZZRoIIwEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSJdWgpfZRoK4+UKGggaIZoiWiLkGgtTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGiGjARUcnVllGggjARoaWdolIwJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowQdmVyaWZpY2F0aW9uX3VybJROjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE5odE51aCl9lGgrj5QoaJ1omGiZaCBoC2iGaJtol2iaaJ5odJBoLU51YowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAtkaXNwb3NpdGlvbpSMDG5vdF9zZWxlY3RlZJR1aCl9lGgrj5QoaKlop5BoLU51YowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGh9aI5ooWh8aK1ormiAaH+QaC1OdWJhjAljaXRhdGlvbnOUXZQoaACMDlJldmlld0NpdGF0aW9ulJOUKYGUfZQoaAV9lChofEsAjAlyZWZlcmVuY2WUjBphIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeZSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIxLVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LCBhY2NvcmRpbmcgdG8gYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnkulIwGcXVvdGVzlF2UjAhwb3NpdGlvbpRoAIwIUG9zaXRpb26Uk5QpgZR9lChoBX2UKIwFc3RhcnSUSwCMA2VuZJRLbowEdGV4dJROdWgpfZRoK4+UKGjHaMhoyZBoLU51YmiAaACMFFJldmlld0NpdGF0aW9uUmVzdWx0lJOUKYGUfZQoaAV9lCiMB2ZpbmRpbmeUjAlzdXBwb3J0ZWSUaImMB3N1cHBvcnSUaIuJdWgpfZRoK4+UKGjRaItoiZBoLU51YowFY2hlY2uUaACME1Jldmlld0NpdGF0aW9uQ2hlY2uUk5QpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUjAlwYWdlX3JlYWSUjARmdWxslIwKcGFnZV90aXRsZZSMJ1BhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZSMAmVulIwKc291cmNlX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjA5zb3VyY2VfdmVyc2lvbpROjAdzdXBwb3J0lIwJc3VwcG9ydGVklIwHc25pcHBldJSME1RoZSBzb3VyY2Ugc2F5cyBzby6UaJeME1RoZSBwYWdlIHN0YXRlcyBpdC6UjAVxdW90ZZROjA1taXNzaW5nX3F1b3RllE6MDmRvaV9yZWdpc3RlcmVklE6MCG1ldGFkYXRhlE6MFG1ldGFkYXRhX2RpZmZlcmVuY2VzlF2UjApyZWdpc3RlcmVklE6MEHVuY2hlY2tlZF9yZWFzb26UTmidTmh0TnVoKX2UaCuPlCho4WjzaJdo4mjkaJ1o7mjvaAto7WjwaOxo32jyaOdo3WjmaOlodJBoLU51YnVoKX2UaCuPlChovWiAaLho1mh8aLxov2i6aMGQaC1OdWJotCmBlH2UKGgFfZQoaHxLAWi4jAthIHN0YXRlbWVudJRouowdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaLxOaL2MIlRoZSBhZ2VuY3kgc2FpZCBzbyBpbiBhIHN0YXRlbWVudC6UaL9dlGjBaMMpgZR9lChoBX2UKGjHS29oyEuyaMlOdWgpfZRoK4+UKGjHaMhoyZBoLU51YmiAaM0pgZR9lChoBX2UKGjRjAlzdXBwb3J0ZWSUaImMB3N1cHBvcnSUaIuJdWgpfZRoK4+UKGjRaItoiZBoLU51YmjWaNgpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaN2MBGZ1bGyUaN+MJVBhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaOFOaOKMAmVulGjkjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRo5k5o54wJc3VwcG9ydGVklGjpjBNUaGUgc291cmNlIHNheXMgc28ulGiXjBNUaGUgcGFnZSBzdGF0ZXMgaXQulGjsTmjtTmjuTmjvTmjwXZRo8k5o805onU5odE51aCl9lGgrj5QoaOFo82iXaOJo5GidaO5o72gLaO1o8GjsaN9o8mjnaN1o5mjpaHSQaC1OdWJ1aCl9lGgrj5QoaL1ogGi4aNZofGi8aL9oumjBkGgtTnViZXVoKX2UaCuPlChobmhpaBFoC2gVaGxoZ2guaHJoXmgPaFpodWgNaHBoE2gJaLFoS2gHaHSQaC1OdWIu", + "review__get_citations_failed_and_unchecked.json": "gASVUA8AAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMCmluY29tcGxldGWUjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lIwbMjAyNi0xMC0wMVQxMDowNzowMC4xMjM0NTZalIwIbGFuZ3VhZ2WUjAJlbpSMBnBvbGljeZRoAIwQRXNjYWxhdGlvblBvbGljeZSTlCmBlH2UKGgFfZQojAh2ZXJkaWN0c5RdlCiMBUZhbHNllIwMTW9zdGx5IEZhbHNllIwFTWl4ZWSUZYwKY29uZmlkZW5jZZRdlIwDbG93lGGMD21heF9hc3Nlc3NtZW50c5RLFIwRbWF4X3ZlcmlmaWNhdGlvbnOUSwWMBWRlcHRolIwIc3RhbmRhcmSUjA1tYXhfY2l0YXRpb25zlEsUjA1zdWdnZXN0X2VkaXRzlIl1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgbaCVoIGgnaChoJGgjkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjA1SZXZpZXdTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NsYWltc19zZWxlY3RlZJRLAYwLY2xhaW1fbGltaXSUSxSME2NsYWltX2xpbWl0X3JlYWNoZWSUiYwPaW5wdXRfdHJ1bmNhdGVklImMC2Fzc2Vzc21lbnRzlGgAjBZSZXZpZXdBc3Nlc3NtZW50Q291bnRzlJOUKYGUfZQoaAV9lCiMCWNvbXBsZXRlZJRLAYwGZmFpbGVklEsAdWgpfZRoK4+UKGg+aD+QaC1OdWKMDXZlcmlmaWNhdGlvbnOUaACMGFJldmlld1ZlcmlmaWNhdGlvbkNvdW50c5STlCmBlH2UKGgFfZQojAdwbGFubmVklEsAaD5LAGg/SwB1aCl9lGgrj5QoaD5oP2hIkGgtTnVijAZpc3N1ZXOUSwCMD2NpdGF0aW9uc19mb3VuZJRLAowSY2l0YXRpb25zX3NlbGVjdGVklEsCjA5jaXRhdGlvbl9saW1pdJRLFIwWY2l0YXRpb25fbGltaXRfcmVhY2hlZJSJjA9jaXRhdGlvbl9jaGVja3OUaACMGVJldmlld0NpdGF0aW9uQ2hlY2tDb3VudHOUk5QpgZR9lChoBX2UKIwHY2hlY2tlZJRLAIwJdW5jaGVja2VklEsBaD9LAXVoKX2UaCuPlChoVmhXaD+QaC1OdWKMD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFtoT2hLaDVoTmhaaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAXNoKX2UaCuPlChoZJBoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhaXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UaACMFVJldmlld0NpdGF0aW9uRmFpbHVyZZSTlCmBlH2UKGgFfZQojA5jaXRhdGlvbl9pbmRleJRLAIwJcmVmZXJlbmNllIwaYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnmUjAljaXRlZF91cmyUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwDZG9plE6MB2ZhaWx1cmWUaACMDEZhaWx1cmVCbG9ja5STlCmBlH2UKGgFfZQojA5mYWlsdXJlX3JlYXNvbpSMCGludGVybmFslIwNZmFpbHVyZV9jbGFzc5SMCGludGVybmFslIwJcmV0cnlhYmxllImMBGhpbnSUjC5UaGUgY2hlY2sgZmFpbGVkIG9uIG91ciBzaWRlLiBUcnkgYWdhaW4gbGF0ZXIulIwIZG9jc191cmyUjCRodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjaW50ZXJuYWyUdWgpfZRoK4+UKGiEaH9ohmiBaIOQaC1OdWJ1aCl9lGgrj5QoaHRoeGhzaHZoeZBoLU51YmGMC21vcmVfY2xhaW1zlF2UjBRtb3JlX2NsYWltX2xvY2F0aW9uc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRoeU6MBmNsYWltc5RdlGgAjAtSZXZpZXdDbGFpbZSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwFY2xhaW2UjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulIwJcG9zaXRpb25zlE6MBnJlc3VsdJRoAIwMUmV2aWV3UmVzdWx0lJOUKYGUfZQoaAV9lCiMB3ZlcmRpY3SUjARUcnVllGggjARoaWdolIwGc291cmNllIwKYXNzZXNzbWVudJSMCGlzX2lzc3VllIl1aCl9lGgrj5QoaCBoo2imaKiQaC1OdWKMCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaKOMBFRydWWUaCCMBGhpZ2iUjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBB2ZXJpZmljYXRpb25fdXJslE6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZRohE6MEXN1Z2dlc3RlZF9yZXdyaXRllE5oeU51aCl9lGgrj5QoaIRotWi2aCBoC2ijaLhotGi3aLpoeZBoLU51YowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAtkaXNwb3NpdGlvbpSMDG5vdF9zZWxlY3RlZJR1aCl9lGgrj5QoaMVow5BoLU51YowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGiaaKtovWiZaMloymidaJyQaC1OdWJhjAljaXRhdGlvbnOUXZQoaACMDlJldmlld0NpdGF0aW9ulJOUKYGUfZQoaAV9lChomUsAaHSMGmEgcmVwb3J0IGZyb20gdGhlIG1pbmlzdHJ5lGh2jB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lGh4TowJc3RhdGVtZW50lIxLVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LCBhY2NvcmRpbmcgdG8gYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnkulIwGcXVvdGVzlF2UjAhwb3NpdGlvbpRoAIwIUG9zaXRpb26Uk5QpgZR9lChoBX2UKIwFc3RhcnSUSwCMA2VuZJRLbowEdGV4dJROdWgpfZRoK4+UKGjgaOFo4pBoLU51YmidTowFY2hlY2uUaACME1Jldmlld0NpdGF0aW9uQ2hlY2uUk5QpgZR9lChoBX2UKGgLjAZmYWlsZWSUjAlwYWdlX3JlYWSUTowKcGFnZV90aXRsZZROjBNwYWdlX3B1Ymxpc2hlZF9kYXRllE6MDXBhZ2VfbGFuZ3VhZ2WUTowKc291cmNlX3VybJROjA5zb3VyY2VfdmVyc2lvbpROjAdzdXBwb3J0lE6MB3NuaXBwZXSUTmi0TowFcXVvdGWUTowNbWlzc2luZ19xdW90ZZROjA5kb2lfcmVnaXN0ZXJlZJROjAhtZXRhZGF0YZROjBRtZXRhZGF0YV9kaWZmZXJlbmNlc5RdlIwKcmVnaXN0ZXJlZJROjBB1bmNoZWNrZWRfcmVhc29ulE5ohIwuVGhlIGNoZWNrIGZhaWxlZCBvbiBvdXIgc2lkZS4gVHJ5IGFnYWluIGxhdGVyLpRoeWh7KYGUfZQoaAV9lChof4wIaW50ZXJuYWyUaIGMCGludGVybmFslGiDiWiEjC5UaGUgY2hlY2sgZmFpbGVkIG9uIG91ciBzaWRlLiBUcnkgYWdhaW4gbGF0ZXIulGiGjCRodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjaW50ZXJuYWyUdWgpfZRoK4+UKGiEaH9ohmiBaIOQaC1OdWJ1aCl9lGgrj5QoaO5o+2i0aO9o8GiEaPZo92gLaPVo+Gj0aO1o+mjyaOxo8WjzaHmQaC1OdWJ1aCl9lGgrj5QoaNZonWh0aOVomWh4aNhodmjakGgtTnViaNApgZR9lChoBX2UKGiZSwFodIwLYSBzdGF0ZW1lbnSUaHaMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGh4TmjWjCJUaGUgYWdlbmN5IHNhaWQgc28gaW4gYSBzdGF0ZW1lbnQulGjYXZRo2mjcKYGUfZQoaAV9lCho4EtvaOFLsmjiTnVoKX2UaCuPlCho4GjhaOKQaC1OdWJonWgAjBRSZXZpZXdDaXRhdGlvblJlc3VsdJSTlCmBlH2UKGgFfZQojAdmaW5kaW5nlIwJdW5jaGVja2VklGimTmioiXVoKX2UaCuPlChqGwEAAGioaKaQaC1OdWJo5WjnKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGjsjARmdWxslGjtjCVQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGjuTmjvjAJlbpRo8IwdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaPFOaPKMCXVuY2hlY2tlZJRo805otIwTVGhlIHBhZ2Ugc3RhdGVzIGl0LpRo9E5o9U5o9k5o905o+F2UaPpOaPuMB25vX3RleHSUaIROaHlOdWgpfZRoK4+UKGjuaPtotGjvaPBohGj2aPdoC2j1aPho9GjtaPpo8mjsaPFo82h5kGgtTnVidWgpfZRoK4+UKGjWaJ1odGjlaJloeGjYaHZo2pBoLU51YmV1aCl9lGgrj5QoaIxoaWgRaAtoFWhsaGdoLmiQaF5oD2haaJJoDWiOaBNoCWjNaEtoB2h5kGgtTnViLg==", + "review__get_citations_issue_full.json": "gASV2w8AAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMDGlzc3Vlc19mb3VuZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLBYwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSxSMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklEsBjAtjbGFpbV9saW1pdJRLFIwTY2xhaW1fbGltaXRfcmVhY2hlZJSJjA9pbnB1dF90cnVuY2F0ZWSUiYwLYXNzZXNzbWVudHOUaACMFlJldmlld0Fzc2Vzc21lbnRDb3VudHOUk5QpgZR9lChoBX2UKIwJY29tcGxldGVklEsBjAZmYWlsZWSUSwB1aCl9lGgrj5QoaD5oP5BoLU51YowNdmVyaWZpY2F0aW9uc5RoAIwYUmV2aWV3VmVyaWZpY2F0aW9uQ291bnRzlJOUKYGUfZQoaAV9lCiMB3BsYW5uZWSUSwBoPksAaD9LAHVoKX2UaCuPlChoPmg/aEiQaC1OdWKMBmlzc3Vlc5RLAIwPY2l0YXRpb25zX2ZvdW5klEsCjBJjaXRhdGlvbnNfc2VsZWN0ZWSUSwKMDmNpdGF0aW9uX2xpbWl0lEsUjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklImMD2NpdGF0aW9uX2NoZWNrc5RoAIwZUmV2aWV3Q2l0YXRpb25DaGVja0NvdW50c5STlCmBlH2UKGgFfZQojAdjaGVja2VklEsCjAl1bmNoZWNrZWSUSwBoP0sAdWgpfZRoK4+UKGhWaFdoP5BoLU51YowPY2l0YXRpb25faXNzdWVzlEsBjBFjaXRhdGlvbnNfc2tpcHBlZJROdWgpfZRoK4+UKGg0aDhoN2hNaEJoW2hPaEtoNWhOaFpoUGhMaDaQaC1OdWKMB2NyZWRpdHOUaACMDVJldmlld0NyZWRpdHOUk5QpgZR9lChoBX2UjAdjaGFyZ2VklEsDc2gpfZRoK4+UKGhkkGgtTnVijBJwb2xsX2FmdGVyX3NlY29uZHOUTmhLXZSMCGZhaWx1cmVzlF2UaFpdlGgAjBNSZXZpZXdDaXRhdGlvbklzc3VllJOUKYGUfZQoaAV9lCiMDmNpdGF0aW9uX2luZGV4lEsAjAlyZWZlcmVuY2WUjBphIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeZSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIxLVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LCBhY2NvcmRpbmcgdG8gYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnkulIwGcXVvdGVzlF2UjAhwb3NpdGlvbpRoAIwIUG9zaXRpb26Uk5QpgZR9lChoBX2UKIwFc3RhcnSUSwCMA2VuZJRLbowEdGV4dJROdWgpfZRoK4+UKGiBaIJog5BoLU51YowHZmluZGluZ5SMDGNvbnRyYWRpY3RlZJSMBnNvdXJjZZSMB3N1cHBvcnSUjAdzbmlwcGV0lIwTVGhlIHNvdXJjZSBzYXlzIHNvLpSMCXJhdGlvbmFsZZSME1RoZSBwYWdlIHN0YXRlcyBpdC6UjA1taXNzaW5nX3F1b3RllE6MFG1ldGFkYXRhX2RpZmZlcmVuY2VzlF2UjApwYWdlX3RpdGxllIwnUGFnZSBhdCBodHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwHZmFpbHVyZZROdWgpfZRoK4+UKGh3aI5ocmiPaJFodmhxaIxoeWiIaIZodGiKaJNoe5BoLU51YmGMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaJNOjAZjbGFpbXOUXZRoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQojAd2ZXJkaWN0lIwEVHJ1ZZRoIIwEaGlnaJRoiIwKYXNzZXNzbWVudJSMCGlzX2lzc3VllIl1aCl9lGgrj5QoaCBor2iIaLOQaC1OdWKMCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaK+MBFRydWWUaCCMBGhpZ2iUaIxOjAdkaXNzZW50lE6MEHZlcmlmaWNhdGlvbl91cmyUTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROaJNOdWgpfZRoK4+UKGjEaL9owGggaAtor2jCaIxowWjFaJOQaC1OdWKMCmVzY2FsYXRpb26UaACMCkVzY2FsYXRpb26Uk5QpgZR9lChoBX2UKIwNbWF0Y2hlZF9ydWxlc5RdlIwLZGlzcG9zaXRpb26UjAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGjQaM6QaC1OdWKMDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKX2UaCuPlChopmi2aMhopWjUaNVoqWiokGgtTnViYYwJY2l0YXRpb25zlF2UKGgAjA5SZXZpZXdDaXRhdGlvbpSTlCmBlH2UKGgFfZQoaKVLAGhyjBphIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeZRodIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJRodk5od4xLVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LCBhY2NvcmRpbmcgdG8gYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnkulGh5XZRoe2h9KYGUfZQoaAV9lChogUsAaIJLbmiDTnVoKX2UaCuPlChogWiCaIOQaC1OdWJoqWgAjBRSZXZpZXdDaXRhdGlvblJlc3VsdJSTlCmBlH2UKGgFfZQoaIaMDGNvbnRyYWRpY3RlZJRoiIwHc3VwcG9ydJRos4h1aCl9lGgrj5QoaIZos2iIkGgtTnVijAVjaGVja5RoAIwTUmV2aWV3Q2l0YXRpb25DaGVja5STlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJSMCXBhZ2VfcmVhZJSMBGZ1bGyUaJGMJ1BhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZSMAmVulIwKc291cmNlX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjA5zb3VyY2VfdmVyc2lvbpROjAdzdXBwb3J0lIwMY29udHJhZGljdGVklGiKjBNUaGUgc291cmNlIHNheXMgc28ulGiMjBNUaGUgcGFnZSBzdGF0ZXMgaXQulIwFcXVvdGWUTmiOTowOZG9pX3JlZ2lzdGVyZWSUTowIbWV0YWRhdGGUTmiPXZSMCnJlZ2lzdGVyZWSUTowQdW5jaGVja2VkX3JlYXNvbpROaMROaJNOdWgpfZRoK4+UKGj7agoBAABojGj8aP5oxGoGAQAAagcBAABoC2iOaI9qBQEAAGiRagkBAABqAQEAAGj4agABAABoimiTkGgtTnVidWgpfZRoK4+UKGh3aKlocmjxaKVodmh5aHRoe5BoLU51YmjbKYGUfZQoaAV9lChopUsBaHKMC2Egc3RhdGVtZW50lGh0jB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRodk5od4wiVGhlIGFnZW5jeSBzYWlkIHNvIGluIGEgc3RhdGVtZW50LpRoeV2UaHtofSmBlH2UKGgFfZQoaIFLb2iCS7Jog051aCl9lGgrj5QoaIFogmiDkGgtTnViaKlo6SmBlH2UKGgFfZQoaIaMCXN1cHBvcnRlZJRoiIwHc3VwcG9ydJRos4l1aCl9lGgrj5QoaIZos2iIkGgtTnViaPFo8ymBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRo+IwEZnVsbJRokYwlUGFnZSBhdCBodHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRo+05o/IwCZW6UaP6MHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGoAAQAATmoBAQAAjAlzdXBwb3J0ZWSUaIqME1RoZSBzb3VyY2Ugc2F5cyBzby6UaIyME1RoZSBwYWdlIHN0YXRlcyBpdC6UagUBAABOaI5OagYBAABOagcBAABOaI9dlGoJAQAATmoKAQAATmjETmiTTnVoKX2UaCuPlCho+2oKAQAAaIxo/Gj+aMRqBgEAAGoHAQAAaAtojmiPagUBAABokWoJAQAAagEBAABo+GoAAQAAaIpok5BoLU51YnVoKX2UaCuPlChod2ipaHJo8WilaHZoeWh0aHuQaC1OdWJldWgpfZRoK4+UKGiYaGloEWgLaBVolmhnaC5onGheaA9oWmieaA1ommgTaAlo2GhLaAdok5BoLU51Yi4=", + "review__get_citations_issue_issues_view.json": "gASV9AcAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjAZpc3N1ZXOUjAZzdGF0dXOUjAljb21wbGV0ZWSUjAdvdXRjb21llIwMaXNzdWVzX2ZvdW5klIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLFIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSxSMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwKMCXVuY2hlY2tlZJRLAGg/SwB1aCl9lGgrj5QoaFZoV2g/kGgtTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwGMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhbaE9oS2g1aE5oWmhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwNzaCl9lGgrj5QoaGSQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlIwIZmFpbHVyZXOUXZRoWl2UaACME1Jldmlld0NpdGF0aW9uSXNzdWWUk5QpgZR9lChoBX2UKIwOY2l0YXRpb25faW5kZXiUSwCMCXJlZmVyZW5jZZSMGmEgcmVwb3J0IGZyb20gdGhlIG1pbmlzdHJ5lIwJY2l0ZWRfdXJslIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMA2RvaZROjAlzdGF0ZW1lbnSUjEtVbmVtcGxveW1lbnQgZmVsbCB0byA0LjElIGluIDIwMjQsIGFjY29yZGluZyB0byBhIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeS6UjAZxdW90ZXOUXZSMCHBvc2l0aW9ulGgAjAhQb3NpdGlvbpSTlCmBlH2UKGgFfZQojAVzdGFydJRLAIwDZW5klEtujAR0ZXh0lE51aCl9lGgrj5QoaIFogmiDkGgtTnVijAdmaW5kaW5nlIwMY29udHJhZGljdGVklIwGc291cmNllIwHc3VwcG9ydJSMB3NuaXBwZXSUjBNUaGUgc291cmNlIHNheXMgc28ulIwJcmF0aW9uYWxllIwTVGhlIHBhZ2Ugc3RhdGVzIGl0LpSMDW1pc3NpbmdfcXVvdGWUTowUbWV0YWRhdGFfZGlmZmVyZW5jZXOUXZSMCnBhZ2VfdGl0bGWUjCdQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjAdmYWlsdXJllE51aCl9lGgrj5QoaHdojmhyaI9okWh2aHFojGh5aIhohmh0aIpok2h7kGgtTnViYYwRY2l0YXRpb25fZmFpbHVyZXOUXZSMC21vcmVfY2xhaW1zlF2UjBRtb3JlX2NsYWltX2xvY2F0aW9uc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRok06MBmNsYWltc5RdlIwJY2l0YXRpb25zlF2UdWgpfZRoK4+UKGiYaGloEWgLaBVolmhnaC5onGheaA9oWmgNaJpoE2gJaEtoB2iTkGgtTnViLg==", + "review__get_citations_limit_exactly_at_limit.json": "gASV2w0AAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAowNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSwKMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwKMCXVuY2hlY2tlZJRLAGg/SwB1aCl9lGgrj5QoaFZoV2g/kGgtTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhbaE9oS2g1aE5oWmhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwNzaCl9lGgrj5QoaGSQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlIwIZmFpbHVyZXOUXZRoWl2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlIwHZmFpbHVyZZROjAZjbGFpbXOUXZRoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQojAd2ZXJkaWN0lIwEVHJ1ZZRoIIwEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSJdWgpfZRoK4+UKGggaIZoiWiLkGgtTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGiGjARUcnVllGggjARoaWdolIwJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowQdmVyaWZpY2F0aW9uX3VybJROjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE5odE51aCl9lGgrj5QoaJ1omGiZaCBoC2iGaJtol2iaaJ5odJBoLU51YowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAtkaXNwb3NpdGlvbpSMDG5vdF9zZWxlY3RlZJR1aCl9lGgrj5QoaKlop5BoLU51YowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGh9aI5ooWh8aK1ormiAaH+QaC1OdWJhjAljaXRhdGlvbnOUXZQoaACMDlJldmlld0NpdGF0aW9ulJOUKYGUfZQoaAV9lChofEsAjAlyZWZlcmVuY2WUjBphIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeZSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIxLVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LCBhY2NvcmRpbmcgdG8gYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnkulIwGcXVvdGVzlF2UjAhwb3NpdGlvbpRoAIwIUG9zaXRpb26Uk5QpgZR9lChoBX2UKIwFc3RhcnSUSwCMA2VuZJRLbowEdGV4dJROdWgpfZRoK4+UKGjHaMhoyZBoLU51YmiAaACMFFJldmlld0NpdGF0aW9uUmVzdWx0lJOUKYGUfZQoaAV9lCiMB2ZpbmRpbmeUjAlzdXBwb3J0ZWSUaImMB3N1cHBvcnSUaIuJdWgpfZRoK4+UKGjRaItoiZBoLU51YowFY2hlY2uUaACME1Jldmlld0NpdGF0aW9uQ2hlY2uUk5QpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUjAlwYWdlX3JlYWSUjARmdWxslIwKcGFnZV90aXRsZZSMJ1BhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZSMAmVulIwKc291cmNlX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjA5zb3VyY2VfdmVyc2lvbpROjAdzdXBwb3J0lIwJc3VwcG9ydGVklIwHc25pcHBldJSME1RoZSBzb3VyY2Ugc2F5cyBzby6UaJeME1RoZSBwYWdlIHN0YXRlcyBpdC6UjAVxdW90ZZROjA1taXNzaW5nX3F1b3RllE6MDmRvaV9yZWdpc3RlcmVklE6MCG1ldGFkYXRhlE6MFG1ldGFkYXRhX2RpZmZlcmVuY2VzlF2UjApyZWdpc3RlcmVklE6MEHVuY2hlY2tlZF9yZWFzb26UTmidTmh0TnVoKX2UaCuPlCho4WjzaJdo4mjkaJ1o7mjvaAto7WjwaOxo32jyaOdo3WjmaOlodJBoLU51YnVoKX2UaCuPlChovWiAaLho1mh8aLxov2i6aMGQaC1OdWJotCmBlH2UKGgFfZQoaHxLAWi4jAthIHN0YXRlbWVudJRouowdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaLxOaL2MIlRoZSBhZ2VuY3kgc2FpZCBzbyBpbiBhIHN0YXRlbWVudC6UaL9dlGjBaMMpgZR9lChoBX2UKGjHS29oyEuyaMlOdWgpfZRoK4+UKGjHaMhoyZBoLU51YmiAaM0pgZR9lChoBX2UKGjRjAlzdXBwb3J0ZWSUaImMB3N1cHBvcnSUaIuJdWgpfZRoK4+UKGjRaItoiZBoLU51YmjWaNgpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaN2MBGZ1bGyUaN+MJVBhZ2UgYXQgaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaOFOaOKMAmVulGjkjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRo5k5o54wJc3VwcG9ydGVklGjpjBNUaGUgc291cmNlIHNheXMgc28ulGiXjBNUaGUgcGFnZSBzdGF0ZXMgaXQulGjsTmjtTmjuTmjvTmjwXZRo8k5o805onU5odE51aCl9lGgrj5QoaOFo82iXaOJo5GidaO5o72gLaO1o8GjsaN9o8mjnaN1o5mjpaHSQaC1OdWJ1aCl9lGgrj5QoaL1ogGi4aNZofGi8aL9oumjBkGgtTnViZXVoKX2UaCuPlChobmhpaBFoC2gVaGxoZ2guaHJoXmgPaFpodWgNaHBoE2gJaLFoS2gHaHSQaC1OdWIu", + "review__get_citations_limit_reached.json": "gASVqQwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAYwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAYwOY2l0YXRpb25fbGltaXSUSwGMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiIwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwGMCXVuY2hlY2tlZJRLAGg/SwB1aCl9lGgrj5QoaFZoV2g/kGgtTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhbaE9oS2g1aE5oWmhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwJzaCl9lGgrj5QoaGSQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlIwIZmFpbHVyZXOUXZRoWl2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlGgAjBJSZXZpZXdNb3JlQ2l0YXRpb26Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwGMCXJlZmVyZW5jZZSMC2Egc3RhdGVtZW50lIwJY2l0ZWRfdXJslIwdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUjANkb2mUTowIc2VudGVuY2WUjCJUaGUgYWdlbmN5IHNhaWQgc28gaW4gYSBzdGF0ZW1lbnQulIwIcG9zaXRpb26UaACMCFBvc2l0aW9ulJOUKYGUfZQoaAV9lCiMBXN0YXJ0lEtvjANlbmSUS7KMBHRleHSUTnVoKX2UaCuPlChoh2iIaImQaC1OdWJ1aCl9lGgrj5QoaHpof2h5aH5ofGiBkGgtTnViYYwHZmFpbHVyZZROjAZjbGFpbXOUXZRoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKGh5SwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQojAd2ZXJkaWN0lIwEVHJ1ZZRoIIwEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSJdWgpfZRoK4+UKGggaJ9oomikkGgtTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGifjARUcnVllGggjARoaWdolIwJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowQdmVyaWZpY2F0aW9uX3VybJROjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE5ojk51aCl9lGgrj5QoaLZosWiyaCBoC2ifaLRosGizaLdojpBoLU51YowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAtkaXNwb3NpdGlvbpSMDG5vdF9zZWxlY3RlZJR1aCl9lGgrj5QoaMJowJBoLU51YowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGiWaKdoumh5aMZox2iZaJiQaC1OdWJhjAljaXRhdGlvbnOUXZRoAIwOUmV2aWV3Q2l0YXRpb26Uk5QpgZR9lChoBX2UKGh5SwBoeowaYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnmUaHyMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUaH5OjAlzdGF0ZW1lbnSUjEtVbmVtcGxveW1lbnQgZmVsbCB0byA0LjElIGluIDIwMjQsIGFjY29yZGluZyB0byBhIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeS6UjAZxdW90ZXOUXZRogWiDKYGUfZQoaAV9lChoh0sAaIhLbmiJTnVoKX2UaCuPlChoh2iIaImQaC1OdWJomWgAjBRSZXZpZXdDaXRhdGlvblJlc3VsdJSTlCmBlH2UKGgFfZQojAdmaW5kaW5nlIwJc3VwcG9ydGVklGiijAdzdXBwb3J0lGikiXVoKX2UaCuPlCho4WikaKKQaC1OdWKMBWNoZWNrlGgAjBNSZXZpZXdDaXRhdGlvbkNoZWNrlJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklIwJcGFnZV9yZWFklIwEZnVsbJSMCnBhZ2VfdGl0bGWUjCdQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjBNwYWdlX3B1Ymxpc2hlZF9kYXRllE6MDXBhZ2VfbGFuZ3VhZ2WUjAJlbpSMCnNvdXJjZV91cmyUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwOc291cmNlX3ZlcnNpb26UTowHc3VwcG9ydJSMCXN1cHBvcnRlZJSMB3NuaXBwZXSUjBNUaGUgc291cmNlIHNheXMgc28ulGiwjBNUaGUgcGFnZSBzdGF0ZXMgaXQulIwFcXVvdGWUTowNbWlzc2luZ19xdW90ZZROjA5kb2lfcmVnaXN0ZXJlZJROjAhtZXRhZGF0YZROjBRtZXRhZGF0YV9kaWZmZXJlbmNlc5RdlIwKcmVnaXN0ZXJlZJROjBB1bmNoZWNrZWRfcmVhc29ulE5otk5ojk51aCl9lGgrj5QoaPFqAwEAAGiwaPJo9Gi2aP5o/2gLaP1qAAEAAGj8aO9qAgEAAGj3aO1o9mj5aI6QaC1OdWJ1aCl9lGgrj5QoaNNomWh6aOZoeWh+aNVofGiBkGgtTnViYXVoKX2UaCuPlChobmhpaBFoC2gVaGxoZ2guaHJoXmgPaFpoj2gNaHBoE2gJaMpoS2gHaI6QaC1OdWIu", + "review__get_citations_no_claim_but_citations.json": "gASVkAsAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLFIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwCMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwCMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSxSMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwKMCXVuY2hlY2tlZJRLAGg/SwB1aCl9lGgrj5QoaFZoV2g/kGgtTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhbaE9oS2g1aE5oWmhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwJzaCl9lGgrj5QoaGSQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlIwIZmFpbHVyZXOUXZRoWl2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlIwHZmFpbHVyZZROjAZjbGFpbXOUXZSMCWNpdGF0aW9uc5RdlChoAIwOUmV2aWV3Q2l0YXRpb26Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMCXJlZmVyZW5jZZSMGmEgcmVwb3J0IGZyb20gdGhlIG1pbmlzdHJ5lIwJY2l0ZWRfdXJslIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMA2RvaZROjAlzdGF0ZW1lbnSUjEtVbmVtcGxveW1lbnQgZmVsbCB0byA0LjElIGluIDIwMjQsIGFjY29yZGluZyB0byBhIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeS6UjAZxdW90ZXOUXZSMCHBvc2l0aW9ulGgAjAhQb3NpdGlvbpSTlCmBlH2UKGgFfZQojAVzdGFydJRLAIwDZW5klEtujAR0ZXh0lE51aCl9lGgrj5QoaI5oj2iQkGgtTnVijAZyZXN1bHSUaACMFFJldmlld0NpdGF0aW9uUmVzdWx0lJOUKYGUfZQoaAV9lCiMB2ZpbmRpbmeUjAlzdXBwb3J0ZWSUjAZzb3VyY2WUjAdzdXBwb3J0lIwIaXNfaXNzdWWUiXVoKX2UaCuPlChomWidaJuQaC1OdWKMBWNoZWNrlGgAjBNSZXZpZXdDaXRhdGlvbkNoZWNrlJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklIwJcGFnZV9yZWFklIwEZnVsbJSMCnBhZ2VfdGl0bGWUjCdQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjBNwYWdlX3B1Ymxpc2hlZF9kYXRllE6MDXBhZ2VfbGFuZ3VhZ2WUjAJlbpSMCnNvdXJjZV91cmyUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwOc291cmNlX3ZlcnNpb26UTowHc3VwcG9ydJSMCXN1cHBvcnRlZJSMB3NuaXBwZXSUjBNUaGUgc291cmNlIHNheXMgc28ulIwJcmF0aW9uYWxllIwTVGhlIHBhZ2Ugc3RhdGVzIGl0LpSMBXF1b3RllE6MDW1pc3NpbmdfcXVvdGWUTowOZG9pX3JlZ2lzdGVyZWSUTowIbWV0YWRhdGGUTowUbWV0YWRhdGFfZGlmZmVyZW5jZXOUXZSMCnJlZ2lzdGVyZWSUTowQdW5jaGVja2VkX3JlYXNvbpROjARoaW50lE5odE51aCl9lGgrj5QoaKtovmi1aKxormi/aLloumgLaLhou2i3aKlovWixaKdosGizaHSQaC1OdWJ1aCl9lGgrj5QoaIRok2h/aKBofmiDaIZogWiIkGgtTnViaHopgZR9lChoBX2UKGh+SwFof4wLYSBzdGF0ZW1lbnSUaIGMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGiDTmiEjCJUaGUgYWdlbmN5IHNhaWQgc28gaW4gYSBzdGF0ZW1lbnQulGiGXZRoiGiKKYGUfZQoaAV9lChojktvaI9LsmiQTnVoKX2UaCuPlChojmiPaJCQaC1OdWJok2iVKYGUfZQoaAV9lChomYwJc3VwcG9ydGVklGibjAdzdXBwb3J0lGidiXVoKX2UaCuPlChomWidaJuQaC1OdWJooGiiKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGinjARmdWxslGipjCVQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGirTmisjAJlbpRorowdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaLBOaLGMCXN1cHBvcnRlZJRos4wTVGhlIHNvdXJjZSBzYXlzIHNvLpRotYwTVGhlIHBhZ2Ugc3RhdGVzIGl0LpRot05ouE5ouU5ouk5ou12UaL1OaL5OaL9OaHROdWgpfZRoK4+UKGiraL5otWisaK5ov2i5aLpoC2i4aLtot2ipaL1osWinaLBos2h0kGgtTnVidWgpfZRoK4+UKGiEaJNof2igaH5og2iGaIFoiJBoLU51YmV1aCl9lGgrj5QoaG5oaWgRaAtoFWhsaGdoLmhyaF5oD2haaHVoDWhwaBNoCWh3aEtoB2h0kGgtTnViLg==", + "review__get_citations_no_claim_no_citation.json": "gASV+AUAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwGZmFpbGVklIwHb3V0Y29tZZSMCXVuY2hlY2tlZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLBYwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSxSMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklE6MC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklE6MD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5ROjA12ZXJpZmljYXRpb25zlE6MBmlzc3Vlc5RLAIwPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJRLFIwWY2l0YXRpb25fbGltaXRfcmVhY2hlZJROjA9jaXRhdGlvbl9jaGVja3OUTowPY2l0YXRpb25faXNzdWVzlEsAjBFjaXRhdGlvbnNfc2tpcHBlZJROdWgpfZRoK4+UKGg0aDhoN2g8aDloQWg+aDpoNWg9aEBoP2g7aDaQaC1OdWKMB2NyZWRpdHOUaACMDVJldmlld0NyZWRpdHOUk5QpgZR9lChoBX2UjAdjaGFyZ2VklEsAc2gpfZRoK4+UKGhKkGgtTnVijBJwb2xsX2FmdGVyX3NlY29uZHOUTmg6XZSMCGZhaWx1cmVzlF2UaEBdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMC21vcmVfY2xhaW1zlE6MFG1vcmVfY2xhaW1fbG9jYXRpb25zlE6MDm1vcmVfY2l0YXRpb25zlE6MB2ZhaWx1cmWUaACMDEZhaWx1cmVCbG9ja5STlCmBlH2UKGgFfZQojA5mYWlsdXJlX3JlYXNvbpSMCG5vX2NsYWltlIwNZmFpbHVyZV9jbGFzc5SMDWludmFsaWRfaW5wdXSUjAlyZXRyeWFibGWUiYwEaGludJSMo05vIGZhY3R1YWwgc3RhdGVtZW50IHRoYXQgY2FuIGJlIGNoZWNrZWQgYWdhaW5zdCBldmlkZW5jZSB3YXMgZm91bmQgaW4gdGhlIGlucHV0LiBTZW5kIG9uZSBmYWN0dWFsIGNsYWltLCBvciBydW4gdGhlIHRleHQgdGhyb3VnaCAvZXh0cmFjdCB0byBlbnVtZXJhdGUgaXRzIGNsYWltcy6UjAhkb2NzX3VybJSMKWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnZhbGlkLWlucHV0lHVoKX2UaCuPlChoYmhdaGRoX2hhkGgtTnVijAZjbGFpbXOUXZSMCWNpdGF0aW9uc5RdlHVoKX2UaCuPlChoVGhPaBFoC2gVaFJoTWguaFZoRGgPaEBoaGgNaFVoE2gJaGpoOmgHaFeQaC1OdWIu", + "review__get_citations_not_asked.json": "gASVtAcAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAXNoKX2UaCuPlChoW5BoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UjAdmYWlsdXJllE6MBmNsYWltc5RdlGgAjAtSZXZpZXdDbGFpbZSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwFY2xhaW2UjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulIwJcG9zaXRpb25zlE6MBnJlc3VsdJRoAIwMUmV2aWV3UmVzdWx0lJOUKYGUfZQoaAV9lCiMB3ZlcmRpY3SUjARUcnVllGggjARoaWdolIwGc291cmNllIwKYXNzZXNzbWVudJSMCGlzX2lzc3VllIl1aCl9lGgrj5QoaCBofWiAaIKQaC1OdWKMCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaH2MBFRydWWUaCCMBGhpZ2iUjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBB2ZXJpZmljYXRpb25fdXJslE6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTmhrTnVoKX2UaCuPlCholGiPaJBoIGgLaH1okmiOaJFolWhrkGgtTnVijAplc2NhbGF0aW9ulGgAjApFc2NhbGF0aW9ulJOUKYGUfZQoaAV9lCiMDW1hdGNoZWRfcnVsZXOUXZSMC2Rpc3Bvc2l0aW9ulIwMbm90X3NlbGVjdGVklHVoKX2UaCuPlChooGiekGgtTnVijAx2ZXJpZmljYXRpb26UTowPc3VnZ2VzdGVkX2VkaXRzlE51aCl9lGgrj5QoaHRohWiYaHNopGilaHdodpBoLU51YmGMCWNpdGF0aW9uc5RdlHVoKX2UaCuPlChoZWhgaBFoC2gVaGNoXmguaGloVWgPaFFobGgNaGdoE2gJaKhoS2gHaGuQaC1OdWIu", + "review__get_citations_only.json": "gASVMAwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSwCMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLFIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwCMC2NsYWltX2xpbWl0lEsAjBNjbGFpbV9saW1pdF9yZWFjaGVklIiMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwCMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSxSMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwKMCXVuY2hlY2tlZJRLAGg/SwB1aCl9lGgrj5QoaFZoV2g/kGgtTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhbaE9oS2g1aE5oWmhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwJzaCl9lGgrj5QoaGSQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlIwIZmFpbHVyZXOUXZRoWl2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UYYwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZRoAIwNQ2xhaW1Mb2NhdGlvbpSTlCmBlH2UKGgFfZQojAVjbGFpbZSMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlwb3NpdGlvbnOUTnVoKX2UaCuPlChoeGh6kGgtTnViYYwObW9yZV9jaXRhdGlvbnOUXZSMB2ZhaWx1cmWUTowGY2xhaW1zlF2UjAljaXRhdGlvbnOUXZQoaACMDlJldmlld0NpdGF0aW9ulJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAjAlyZWZlcmVuY2WUjBphIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeZSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIxLVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LCBhY2NvcmRpbmcgdG8gYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnkulIwGcXVvdGVzlF2UjAhwb3NpdGlvbpRoAIwIUG9zaXRpb26Uk5QpgZR9lChoBX2UKIwFc3RhcnSUSwCMA2VuZJRLbowEdGV4dJROdWgpfZRoK4+UKGiZaJpom5BoLU51YowGcmVzdWx0lGgAjBRSZXZpZXdDaXRhdGlvblJlc3VsdJSTlCmBlH2UKGgFfZQojAdmaW5kaW5nlIwJc3VwcG9ydGVklIwGc291cmNllIwHc3VwcG9ydJSMCGlzX2lzc3VllIl1aCl9lGgrj5QoaKRoqGimkGgtTnVijAVjaGVja5RoAIwTUmV2aWV3Q2l0YXRpb25DaGVja5STlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJSMCXBhZ2VfcmVhZJSMBGZ1bGyUjApwYWdlX3RpdGxllIwnUGFnZSBhdCBodHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwTcGFnZV9wdWJsaXNoZWRfZGF0ZZROjA1wYWdlX2xhbmd1YWdllIwCZW6UjApzb3VyY2VfdXJslIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMDnNvdXJjZV92ZXJzaW9ulE6MB3N1cHBvcnSUjAlzdXBwb3J0ZWSUjAdzbmlwcGV0lIwTVGhlIHNvdXJjZSBzYXlzIHNvLpSMCXJhdGlvbmFsZZSME1RoZSBwYWdlIHN0YXRlcyBpdC6UjAVxdW90ZZROjA1taXNzaW5nX3F1b3RllE6MDmRvaV9yZWdpc3RlcmVklE6MCG1ldGFkYXRhlE6MFG1ldGFkYXRhX2RpZmZlcmVuY2VzlF2UjApyZWdpc3RlcmVklE6MEHVuY2hlY2tlZF9yZWFzb26UTowEaGludJROaH9OdWgpfZRoK4+UKGi2aMlowGi3aLloymjEaMVoC2jDaMZowmi0aMhovGiyaLtovmh/kGgtTnVidWgpfZRoK4+UKGiPaJ5oimiraIlojmiRaIxok5BoLU51YmiFKYGUfZQoaAV9lChoiUsBaIqMC2Egc3RhdGVtZW50lGiMjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRojk5oj4wiVGhlIGFnZW5jeSBzYWlkIHNvIGluIGEgc3RhdGVtZW50LpRokV2UaJNolSmBlH2UKGgFfZQoaJlLb2iaS7Jom051aCl9lGgrj5QoaJlommibkGgtTnViaJ5ooCmBlH2UKGgFfZQoaKSMCXN1cHBvcnRlZJRopowHc3VwcG9ydJRoqIl1aCl9lGgrj5QoaKRoqGimkGgtTnViaKtorSmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRosowEZnVsbJRotIwlUGFnZSBhdCBodHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRotk5ot4wCZW6UaLmMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGi7Tmi8jAlzdXBwb3J0ZWSUaL6ME1RoZSBzb3VyY2Ugc2F5cyBzby6UaMCME1RoZSBwYWdlIHN0YXRlcyBpdC6UaMJOaMNOaMROaMVOaMZdlGjITmjJTmjKTmh/TnVoKX2UaCuPlChotmjJaMBot2i5aMpoxGjFaAtow2jGaMJotGjIaLxosmi7aL5of5BoLU51YnVoKX2UaCuPlChoj2ieaIpoq2iJaI5okWiMaJOQaC1OdWJldWgpfZRoK4+UKGhuaGloEWgLaBVobGhnaC5ofWheaA9oWmiAaA1ocWgTaAlogmhLaAdof5BoLU51Yi4=", + "review__get_citations_partly_supported.json": "gASV6Q0AAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLFIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSxSMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwKMCXVuY2hlY2tlZJRLAGg/SwB1aCl9lGgrj5QoaFZoV2g/kGgtTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhbaE9oS2g1aE5oWmhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwNzaCl9lGgrj5QoaGSQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlIwIZmFpbHVyZXOUXZRoWl2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlIwHZmFpbHVyZZROjAZjbGFpbXOUXZRoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQojAd2ZXJkaWN0lIwEVHJ1ZZRoIIwEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSJdWgpfZRoK4+UKGggaIZoiWiLkGgtTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGiGjARUcnVllGggjARoaWdolIwJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowQdmVyaWZpY2F0aW9uX3VybJROjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE5odE51aCl9lGgrj5QoaJ1omGiZaCBoC2iGaJtol2iaaJ5odJBoLU51YowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAtkaXNwb3NpdGlvbpSMDG5vdF9zZWxlY3RlZJR1aCl9lGgrj5QoaKlop5BoLU51YowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGh9aI5ooWh8aK1ormiAaH+QaC1OdWJhjAljaXRhdGlvbnOUXZQoaACMDlJldmlld0NpdGF0aW9ulJOUKYGUfZQoaAV9lChofEsAjAlyZWZlcmVuY2WUjBphIHJlcG9ydCBmcm9tIHRoZSBtaW5pc3RyeZSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIxLVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LCBhY2NvcmRpbmcgdG8gYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnkulIwGcXVvdGVzlF2UjAhwb3NpdGlvbpRoAIwIUG9zaXRpb26Uk5QpgZR9lChoBX2UKIwFc3RhcnSUSwCMA2VuZJRLbowEdGV4dJROdWgpfZRoK4+UKGjHaMhoyZBoLU51YmiAaACMFFJldmlld0NpdGF0aW9uUmVzdWx0lJOUKYGUfZQoaAV9lCiMB2ZpbmRpbmeUjBBwYXJ0bHlfc3VwcG9ydGVklGiJjAdzdXBwb3J0lGiLiXVoKX2UaCuPlCho0WiLaImQaC1OdWKMBWNoZWNrlGgAjBNSZXZpZXdDaXRhdGlvbkNoZWNrlJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklIwJcGFnZV9yZWFklIwEZnVsbJSMCnBhZ2VfdGl0bGWUjCdQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjBNwYWdlX3B1Ymxpc2hlZF9kYXRllE6MDXBhZ2VfbGFuZ3VhZ2WUjAJlbpSMCnNvdXJjZV91cmyUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwOc291cmNlX3ZlcnNpb26UTowHc3VwcG9ydJSMEHBhcnRseV9zdXBwb3J0ZWSUjAdzbmlwcGV0lIwTVGhlIHNvdXJjZSBzYXlzIHNvLpRol4wTVGhlIHBhZ2Ugc3RhdGVzIGl0LpSMBXF1b3RllE6MDW1pc3NpbmdfcXVvdGWUTowOZG9pX3JlZ2lzdGVyZWSUTowIbWV0YWRhdGGUTowUbWV0YWRhdGFfZGlmZmVyZW5jZXOUXZSMCnJlZ2lzdGVyZWSUTowQdW5jaGVja2VkX3JlYXNvbpROaJ1OaHROdWgpfZRoK4+UKGjhaPNol2jiaORonWjuaO9oC2jtaPBo7GjfaPJo52jdaOZo6Wh0kGgtTnVidWgpfZRoK4+UKGi9aIBouGjWaHxovGi/aLpowZBoLU51Ymi0KYGUfZQoaAV9lChofEsBaLiMC2Egc3RhdGVtZW50lGi6jB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRovE5ovYwiVGhlIGFnZW5jeSBzYWlkIHNvIGluIGEgc3RhdGVtZW50LpRov12UaMFowymBlH2UKGgFfZQoaMdLb2jIS7JoyU51aCl9lGgrj5QoaMdoyGjJkGgtTnViaIBozSmBlH2UKGgFfZQoaNGMCXN1cHBvcnRlZJRoiYwHc3VwcG9ydJRoi4l1aCl9lGgrj5QoaNFoi2iJkGgtTnViaNZo2CmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRo3YwEZnVsbJRo34wlUGFnZSBhdCBodHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRo4U5o4owCZW6UaOSMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGjmTmjnjAlzdXBwb3J0ZWSUaOmME1RoZSBzb3VyY2Ugc2F5cyBzby6UaJeME1RoZSBwYWdlIHN0YXRlcyBpdC6UaOxOaO1OaO5OaO9OaPBdlGjyTmjzTmidTmh0TnVoKX2UaCuPlCho4WjzaJdo4mjkaJ1o7mjvaAto7WjwaOxo32jyaOdo3WjmaOlodJBoLU51YnVoKX2UaCuPlChovWiAaLho1mh8aLxov2i6aMGQaC1OdWJldWgpfZRoK4+UKGhuaGloEWgLaBVobGhnaC5ocmheaA9oWmh1aA1ocGgTaAlosWhLaAdodJBoLU51Yi4=", + "review__get_citations_running.json": "gASVIAsAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJdmVyaWZ5aW5nlIwHb3V0Y29tZZROjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLFIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGWgjaB5oJWgmaCJoIZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoJ32UaCmPlChoPGg9kGgrTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg8SwBoPUsAdWgnfZRoKY+UKGg8aD1oRpBoK051YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUSwKMEmNpdGF0aW9uc19zZWxlY3RlZJRLAowOY2l0YXRpb25fbGltaXSUSxSMFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUiYwPY2l0YXRpb25fY2hlY2tzlGgAjBlSZXZpZXdDaXRhdGlvbkNoZWNrQ291bnRzlJOUKYGUfZQoaAV9lCiMB2NoZWNrZWSUSwCMCXVuY2hlY2tlZJRLAGg9SwB1aCd9lGgpj5QoaFRoVWg9kGgrTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCd9lGgpj5QoaDJoNmg1aEtoQGhZaE1oSWgzaExoWGhOaEpoNJBoK051YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwFzaCd9lGgpj5QoaGKQaCtOdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5RLD2hJXZSMCGZhaWx1cmVzlF2UaFhdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMC21vcmVfY2xhaW1zlF2UjBRtb3JlX2NsYWltX2xvY2F0aW9uc5RdlIwObW9yZV9jaXRhdGlvbnOUXZSMB2ZhaWx1cmWUTowGY2xhaW1zlF2UaACMC1Jldmlld0NsYWltlJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAjAVjbGFpbZSMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKIwHdmVyZGljdJSMBFRydWWUaB6MBGhpZ2iUjAZzb3VyY2WUjAphc3Nlc3NtZW50lIwIaXNfaXNzdWWUiXVoJ32UaCmPlChoHmiEaIdoiZBoK051YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRohIwEVHJ1ZZRoHowEaGlnaJSMCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEHZlcmlmaWNhdGlvbl91cmyUTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROaHJOdWgnfZRoKY+UKGibaJZol2geaAtohGiZaJVomGicaHKQaCtOdWKMCmVzY2FsYXRpb26UaACMCkVzY2FsYXRpb26Uk5QpgZR9lChoBX2UKIwNbWF0Y2hlZF9ydWxlc5RdlIwLZGlzcG9zaXRpb26UjAxub3Rfc2VsZWN0ZWSUdWgnfZRoKY+UKGinaKWQaCtOdWKMDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoJ32UaCmPlChoe2iMaJ9oemiraKxofmh9kGgrTnViYYwJY2l0YXRpb25zlF2UKGgAjA5SZXZpZXdDaXRhdGlvbpSTlCmBlH2UKGgFfZQoaHpLAIwJcmVmZXJlbmNllIwaYSByZXBvcnQgZnJvbSB0aGUgbWluaXN0cnmUjAljaXRlZF91cmyUjB9odHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwDZG9plE6MCXN0YXRlbWVudJROjAZxdW90ZXOUXZSMCHBvc2l0aW9ulE5ofk6MBWNoZWNrlGgAjBNSZXZpZXdDaXRhdGlvbkNoZWNrlJOUKYGUfZQoaAV9lChoC4wHcGVuZGluZ5SMCXBhZ2VfcmVhZJROjApwYWdlX3RpdGxllE6ME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZROjApzb3VyY2VfdXJslE6MDnNvdXJjZV92ZXJzaW9ulE6MB3N1cHBvcnSUTowHc25pcHBldJROaJVOjAVxdW90ZZROjA1taXNzaW5nX3F1b3RllE6MDmRvaV9yZWdpc3RlcmVklE6MCG1ldGFkYXRhlE6MFG1ldGFkYXRhX2RpZmZlcmVuY2VzlF2UjApyZWdpc3RlcmVklE6MEHVuY2hlY2tlZF9yZWFzb26UTmibTmhyTnVoJ32UaCmPlChoyGjVaJVoyWjKaJto0GjRaAtoz2jSaM5ox2jUaMxoxmjLaM1ocpBoK051YnVoJ32UaCmPlChou2h+aLZov2h6aLpovGi4aL6QaCtOdWJosimBlH2UKGgFfZQoaHpLAWi2jAthIHN0YXRlbWVudJRouIwdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaLpOaLtOaLxdlGi+Tmh+Tmi/aMEpgZR9lChoBX2UKGgLjAdwZW5kaW5nlGjGTmjHTmjITmjJTmjKTmjLTmjMTmjNTmiVTmjOTmjPTmjQTmjRTmjSXZRo1E5o1U5om05ock51aCd9lGgpj5QoaMho1WiVaMloymibaNBo0WgLaM9o0mjOaMdo1GjMaMZoy2jNaHKQaCtOdWJ1aCd9lGgpj5QoaLtofmi2aL9oemi6aLxouGi+kGgrTnViZXVoJ32UaCmPlChobGhnaBBoC2gTaGpoZWgsaHBoXGgOaFhoc2gNaG5oEWgJaK9oSWgHaHKQaCtOdWIu", + "review__get_citations_skipped_switched_off.json": "gASVwwcAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLFIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lEsUjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklIwMc3dpdGNoZWRfb2ZmlHVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAXNoKX2UaCuPlChoXJBoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UjAdmYWlsdXJllE6MBmNsYWltc5RdlGgAjAtSZXZpZXdDbGFpbZSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwFY2xhaW2UjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulIwJcG9zaXRpb25zlE6MBnJlc3VsdJRoAIwMUmV2aWV3UmVzdWx0lJOUKYGUfZQoaAV9lCiMB3ZlcmRpY3SUjARUcnVllGggjARoaWdolIwGc291cmNllIwKYXNzZXNzbWVudJSMCGlzX2lzc3VllIl1aCl9lGgrj5QoaCBofmiBaIOQaC1OdWKMCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaH6MBFRydWWUaCCMBGhpZ2iUjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBB2ZXJpZmljYXRpb25fdXJslE6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTmhsTnVoKX2UaCuPlCholWiQaJFoIGgLaH5ok2iPaJJolmhskGgtTnVijAplc2NhbGF0aW9ulGgAjApFc2NhbGF0aW9ulJOUKYGUfZQoaAV9lCiMDW1hdGNoZWRfcnVsZXOUXZSMC2Rpc3Bvc2l0aW9ulIwMbm90X3NlbGVjdGVklHVoKX2UaCuPlChooWifkGgtTnVijAx2ZXJpZmljYXRpb26UTowPc3VnZ2VzdGVkX2VkaXRzlE51aCl9lGgrj5QoaHVohmiZaHRopWimaHhod5BoLU51YmGMCWNpdGF0aW9uc5RdlHVoKX2UaCuPlChoZmhhaBFoC2gVaGRoX2guaGpoVmgPaFFobWgNaGhoE2gJaKloS2gHaGyQaC1OdWIu", + "review__get_citations_skipped_url_input.json": "gASVeAUAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwGZmFpbGVklIwHb3V0Y29tZZSMCXVuY2hlY2tlZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLBYwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSxSMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklE6MC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklE6MD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5ROjA12ZXJpZmljYXRpb25zlE6MBmlzc3Vlc5RLAIwPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJRLFIwWY2l0YXRpb25fbGltaXRfcmVhY2hlZJROjA9jaXRhdGlvbl9jaGVja3OUTowPY2l0YXRpb25faXNzdWVzlEsAjBFjaXRhdGlvbnNfc2tpcHBlZJSMCXVybF9pbnB1dJR1aCl9lGgrj5QoaDRoOGg3aDxoOWhBaD5oOmg1aD1oQGg/aDtoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwBzaCl9lGgrj5QoaEuQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaDpdlIwIZmFpbHVyZXOUXZRoQF2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUTowUbW9yZV9jbGFpbV9sb2NhdGlvbnOUTowObW9yZV9jaXRhdGlvbnOUTowHZmFpbHVyZZRoAIwMRmFpbHVyZUJsb2NrlJOUKYGUfZQoaAV9lCiMDmZhaWx1cmVfcmVhc29ulIwUdXBzdHJlYW1fdW5hdmFpbGFibGWUjA1mYWlsdXJlX2NsYXNzlIwUdXBzdHJlYW1fdW5hdmFpbGFibGWUjAlyZXRyeWFibGWUiIwEaGludJROjAhkb2NzX3VybJSMMGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyN1cHN0cmVhbS11bmF2YWlsYWJsZZR1aCl9lGgrj5QoaGNoXmhkaGBoYpBoLU51YowGY2xhaW1zlF2UjAljaXRhdGlvbnOUXZR1aCl9lGgrj5QoaFVoUGgRaAtoFWhTaE5oLmhXaEVoD2hAaGhoDWhWaBNoCWhqaDpoB2hYkGgtTnViLg==", + "review__get_claim_limit_not_reached.json": "gASVBwkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSwOMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwKMC2NsYWltX2xpbWl0lEsDjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwKMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoKX2UaCuPlChoW5BoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UjAdmYWlsdXJllE6MBmNsYWltc5RdlChoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQojAd2ZXJkaWN0lIwEVHJ1ZZRoIIwEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSJdWgpfZRoK4+UKGggaH1ogGiCkGgtTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGh9jARUcnVllGggjARoaWdolIwJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowQdmVyaWZpY2F0aW9uX3VybJROjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE5oa051aCl9lGgrj5QoaJRoj2iQaCBoC2h9aJJojmiRaJVoa5BoLU51YowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAtkaXNwb3NpdGlvbpSMDG5vdF9zZWxlY3RlZJR1aCl9lGgrj5QoaKBonpBoLU51YowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGh0aIVomGhzaKRopWh3aHaQaC1OdWJobymBlH2UKGgFfZQoaHNLAWh0jCJXYXRlciBib2lscyBhdCAxMDAgQyBhdCBzZWEgbGV2ZWwulGh2Tmh3aHkpgZR9lChoBX2UKGh9jARUcnVllGggjARoaWdolGiAjAphc3Nlc3NtZW50lGiCiXVoKX2UaCuPlChoIGh9aIBogpBoLU51YmiFaIcpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaH2MBFRydWWUaCCMBGhpZ2iUaI5OaI9OaJBOaJFOaJJdlGiUTmiVTmhrTnVoKX2UaCuPlCholGiPaJBoIGgLaH1okmiOaJFolWhrkGgtTnViaJhomimBlH2UKGgFfZQoaJ5dlGigjAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGigaJ6QaC1OdWJopE5opU51aCl9lGgrj5QoaHRohWiYaHNopGilaHdodpBoLU51YmWMCWNpdGF0aW9uc5RdlHVoKX2UaCuPlChoZWhgaBFoC2gVaGNoXmguaGloVWgPaFFobGgNaGdoE2gJaMZoS2gHaGuQaC1OdWIu", + "review__get_claim_limit_reached_exact.json": "gASVBwkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSwKMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwKMC2NsYWltX2xpbWl0lEsCjBNjbGFpbV9saW1pdF9yZWFjaGVklIiMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwKMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoKX2UaCuPlChoW5BoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UjAdmYWlsdXJllE6MBmNsYWltc5RdlChoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQojAd2ZXJkaWN0lIwEVHJ1ZZRoIIwEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSJdWgpfZRoK4+UKGggaH1ogGiCkGgtTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGh9jARUcnVllGggjARoaWdolIwJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowQdmVyaWZpY2F0aW9uX3VybJROjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE5oa051aCl9lGgrj5QoaJRoj2iQaCBoC2h9aJJojmiRaJVoa5BoLU51YowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAtkaXNwb3NpdGlvbpSMDG5vdF9zZWxlY3RlZJR1aCl9lGgrj5QoaKBonpBoLU51YowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGh0aIVomGhzaKRopWh3aHaQaC1OdWJobymBlH2UKGgFfZQoaHNLAWh0jCJXYXRlciBib2lscyBhdCAxMDAgQyBhdCBzZWEgbGV2ZWwulGh2Tmh3aHkpgZR9lChoBX2UKGh9jARUcnVllGggjARoaWdolGiAjAphc3Nlc3NtZW50lGiCiXVoKX2UaCuPlChoIGh9aIBogpBoLU51YmiFaIcpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaH2MBFRydWWUaCCMBGhpZ2iUaI5OaI9OaJBOaJFOaJJdlGiUTmiVTmhrTnVoKX2UaCuPlCholGiPaJBoIGgLaH1okmiOaJFolWhrkGgtTnViaJhomimBlH2UKGgFfZQoaJ5dlGigjAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGigaJ6QaC1OdWJopE5opU51aCl9lGgrj5QoaHRohWiYaHNopGilaHdodpBoLU51YmWMCWNpdGF0aW9uc5RdlHVoKX2UaCuPlChoZWhgaBFoC2gVaGNoXmguaGloVWgPaFFobGgNaGdoE2gJaMZoS2gHaGuQaC1OdWIu", + "review__get_completed_assess_only.json": "gASVTAoAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMDGlzc3Vlc19mb3VuZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLAIwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSwCMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklEsCjAtjbGFpbV9saW1pdJRLFIwTY2xhaW1fbGltaXRfcmVhY2hlZJSJjA9pbnB1dF90cnVuY2F0ZWSUiYwLYXNzZXNzbWVudHOUaACMFlJldmlld0Fzc2Vzc21lbnRDb3VudHOUk5QpgZR9lChoBX2UKIwJY29tcGxldGVklEsCjAZmYWlsZWSUSwB1aCl9lGgrj5QoaD5oP5BoLU51YowNdmVyaWZpY2F0aW9uc5RoAIwYUmV2aWV3VmVyaWZpY2F0aW9uQ291bnRzlJOUKYGUfZQoaAV9lCiMB3BsYW5uZWSUSwBoPksAaD9LAHVoKX2UaCuPlChoPmg/aEiQaC1OdWKMBmlzc3Vlc5RLAYwPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJROjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhSaE9oS2g1aE5oUWhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwJzaCl9lGgrj5QoaFuQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlGgAjAtSZXZpZXdJc3N1ZZSTlCmBlH2UKGgFfZQojAtjbGFpbV9pbmRleJRLAYwFY2xhaW2UjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjA52ZXJpZmllZF9jbGFpbZROjAd2ZXJkaWN0lIwFRmFsc2WUaCCMBGhpZ2iUjAZzb3VyY2WUjAphc3Nlc3NtZW50lIwPdmVyaWZpY2F0aW9uX2lklE6ME3ZlcmlmaWNhdGlvbl9zdGF0dXOUTowQdmVyaWZpY2F0aW9uX3VybJROjAN1cmyUTowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAd2ZXJkaWN0lGGMC2Rpc3Bvc2l0aW9ulIwDY2FwlHVoKX2UaCuPlChoe2h4kGgtTnVijAtrZXlfZmluZGluZ5ROjAlyYXRpb25hbGWUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTowHZmFpbHVyZZROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKX2UaCuPlChoZmhuaHBoIGhpaG9ocmiAaINocWhsaGhoZWh/aIFogpBoLU51YmGMCGZhaWx1cmVzlF2UaFFdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMC21vcmVfY2xhaW1zlF2UjBRtb3JlX2NsYWltX2xvY2F0aW9uc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRogk6MBmNsYWltc5RdlChoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwBoZowoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQoaGmMBFRydWWUaCCMBGhpZ2iUaGyMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSJdWgpfZRoK4+UKGggaGlobGikkGgtTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGhpjARUcnVllGggjARoaWdolGiATowHZGlzc2VudJROaHBOjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE5ogU5ogk51aCl9lGgrj5QoaLRosGhwaCBoC2hpaLJogGixaIFogpBoLU51YmhyaHQpgZR9lChoBX2UKGh4XZRoe4wMbm90X3NlbGVjdGVklHVoKX2UaCuPlChoe2h4kGgtTnVijAx2ZXJpZmljYXRpb26UTmiDTnVoKX2UaCuPlChoZminaHJomGi+aINom2iakGgtTnViaJQpgZR9lChoBX2UKGiYSwFoZoweVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulGiaTmibaJ0pgZR9lChoBX2UKGhpjAVGYWxzZZRoIIwEaGlnaJRobIwKYXNzZXNzbWVudJRopIh1aCl9lGgrj5QoaCBoaWhsaKSQaC1OdWJop2ipKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGhpjAVGYWxzZZRoIIwEaGlnaJRogE5osE5ocE5osU5osl2UaLROaIFOaIJOdWgpfZRoK4+UKGi0aLBocGggaAtoaWiyaIBosWiBaIKQaC1OdWJocmh0KYGUfZQoaAV9lChoeF2UjAd2ZXJkaWN0lGFoe4wDY2FwlHVoKX2UaCuPlChoe2h4kGgtTnViaL5OaINOdWgpfZRoK4+UKGhmaKdocmiYaL5og2ibaJqQaC1OdWJljAljaXRhdGlvbnOUXZR1aCl9lGgrj5QoaItohmgRaAtoFWiJaF5oLmiPaFVoD2hRaJFoDWiNaBNoCWjgaEtoB2iCkGgtTnViLg==", + "review__get_completed_clean.json": "gASVBAkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwKMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwKMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoKX2UaCuPlChoW5BoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UjAdmYWlsdXJllE6MBmNsYWltc5RdlChoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQojAd2ZXJkaWN0lIwEVHJ1ZZRoIIwEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSJdWgpfZRoK4+UKGggaH1ogGiCkGgtTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGh9jARUcnVllGggjARoaWdolIwJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowQdmVyaWZpY2F0aW9uX3VybJROjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE5oa051aCl9lGgrj5QoaJRoj2iQaCBoC2h9aJJojmiRaJVoa5BoLU51YowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAtkaXNwb3NpdGlvbpSMDG5vdF9zZWxlY3RlZJR1aCl9lGgrj5QoaKBonpBoLU51YowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGh0aIVomGhzaKRopWh3aHaQaC1OdWJobymBlH2UKGgFfZQoaHNLAWh0jB9QYXJpcyBpcyB0aGUgY2FwaXRhbCBvZiBGcmFuY2UulGh2Tmh3aHkpgZR9lChoBX2UKGh9jARUcnVllGggjARoaWdolGiAjAphc3Nlc3NtZW50lGiCiXVoKX2UaCuPlChoIGh9aIBogpBoLU51YmiFaIcpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaH2MBFRydWWUaCCMBGhpZ2iUaI5OaI9OaJBOaJFOaJJdlGiUTmiVTmhrTnVoKX2UaCuPlCholGiPaJBoIGgLaH1okmiOaJFolWhrkGgtTnViaJhomimBlH2UKGgFfZQoaJ5dlGigjAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGigaJ6QaC1OdWJopE5opU51aCl9lGgrj5QoaHRohWiYaHNopGilaHdodpBoLU51YmWMCWNpdGF0aW9uc5RdlHVoKX2UaCuPlChoZWhgaBFoC2gVaGNoXmguaGloVWgPaFFobGgNaGdoE2gJaMZoS2gHaGuQaC1OdWIu", + "review__get_completed_issues_all_verified.json": "gASVIhMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMDGlzc3Vlc19mb3VuZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLBYwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSwCMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklEsDjAtjbGFpbV9saW1pdJRLFIwTY2xhaW1fbGltaXRfcmVhY2hlZJSJjA9pbnB1dF90cnVuY2F0ZWSUiYwLYXNzZXNzbWVudHOUaACMFlJldmlld0Fzc2Vzc21lbnRDb3VudHOUk5QpgZR9lChoBX2UKIwJY29tcGxldGVklEsDjAZmYWlsZWSUSwB1aCl9lGgrj5QoaD5oP5BoLU51YowNdmVyaWZpY2F0aW9uc5RoAIwYUmV2aWV3VmVyaWZpY2F0aW9uQ291bnRzlJOUKYGUfZQoaAV9lCiMB3BsYW5uZWSUSwJoPksCaD9LAHVoKX2UaCuPlChoPmg/aEiQaC1OdWKMBmlzc3Vlc5RLAowPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJROjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhSaE9oS2g1aE5oUWhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSxdzaCl9lGgrj5QoaFuQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlChoAIwLUmV2aWV3SXNzdWWUk5QpgZR9lChoBX2UKIwLY2xhaW1faW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMDnZlcmlmaWVkX2NsYWltlE6MB3ZlcmRpY3SUjAVGYWxzZZRoIIwEaGlnaJSMBnNvdXJjZZSMDHZlcmlmaWNhdGlvbpSMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwTdmVyaWZpY2F0aW9uX3N0YXR1c5SMCWNvbXBsZXRlZJSMEHZlcmlmaWNhdGlvbl91cmyUjC1odHRwczovL2xlbnouaW8vYXBpL3YxL3ZlcmlmaWNhdGlvbnMvNjhjYTRhYWaUjAN1cmyUjC1odHRwczovL2xlbnouaW8vYy90aGUtZWFydGgtaXMtcm91bmQtNjhjYTRhYWaUjAplc2NhbGF0aW9ulGgAjApFc2NhbGF0aW9ulJOUKYGUfZQoaAV9lCiMDW1hdGNoZWRfcnVsZXOUXZSMB3ZlcmRpY3SUYYwLZGlzcG9zaXRpb26UjAdwbGFubmVklHVoKX2UaCuPlChof2h8kGgtTnVijAtrZXlfZmluZGluZ5SMNEZpbmRpbmcgZm9yIFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlyYXRpb25hbGWUTowRc3VnZ2VzdGVkX3Jld3JpdGWUjC9GaXhlZDogVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMB2ZhaWx1cmWUTowPc3VnZ2VzdGVkX2VkaXRzlE51aCl9lGgrj5QoaGZobmhyaCBoaWhwaHZohWiJaHRobGhoaGVog2iGaIiQaC1OdWJoYSmBlH2UKGgFfZQoaGVLAWhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UaGhOaGmMBUZhbHNllGggjARoaWdolGhsjAx2ZXJpZmljYXRpb26UaG6MCDRkNDRkZmFmlGhwjAljb21wbGV0ZWSUaHKMLWh0dHBzOi8vbGVuei5pby9hcGkvdjEvdmVyaWZpY2F0aW9ucy80ZDQ0ZGZhZpRodIwtaHR0cHM6Ly9sZW56LmlvL2MvdGhlLWVhcnRoLWlzLXJvdW5kLTRkNDRkZmFmlGh2aHgpgZR9lChoBX2UKGh8XZSMB3ZlcmRpY3SUYWh/jAdwbGFubmVklHVoKX2UaCuPlChof2h8kGgtTnViaIOMKkZpbmRpbmcgZm9yIFRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpRohU5ohowlRml4ZWQ6IFRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpRoiE5oiU51aCl9lGgrj5QoaGZobmhyaCBoaWhwaHZohWiJaHRobGhoaGVog2iGaIiQaC1OdWJljAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaIhOjAZjbGFpbXOUXZQoaACMC1Jldmlld0NsYWltlJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAaGaMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKGhpjAVGYWxzZZRoIIwEaGlnaJRobIwMdmVyaWZpY2F0aW9ulIwIaXNfaXNzdWWUiHVoKX2UaCuPlChoIGhpaGxowZBoLU51YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRoaYwMTW9zdGx5IEZhbHNllGggjAZtZWRpdW2UaIVOjAdkaXNzZW50lE5ock6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTmiGTmiITnVoKX2UaCuPlCho0WjNaHJoIGgLaGloz2iFaM5ohmiIkGgtTnViaHZoeCmBlH2UKGgFfZQoaHxdlIwHdmVyZGljdJRhaH+MB3BsYW5uZWSUdWgpfZRoK4+UKGh/aHyQaC1OdWKMDHZlcmlmaWNhdGlvbpRoAIwSUmV2aWV3VmVyaWZpY2F0aW9ulJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklIwOY29udGVudF9zdGF0dXOUjAlhdmFpbGFibGWUaG6MCDY4Y2E0YWFmlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklGhmjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulGgTjAJlbpSMCnZpc2liaWxpdHmUjAdwcml2YXRllGgljAhzdGFuZGFyZJSMBmRvbWFpbpSMAJSMCGVudGl0aWVzlF2UaGmMBUZhbHNllGggjARoaWdolIwKbGVuel9zY29yZZRLAmiDjDRGaW5kaW5nIGZvciBUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulIwRZXhlY3V0aXZlX3N1bW1hcnmUjDRTdW1tYXJ5IGZvciBUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulGiGjC9GaXhlZDogVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCHdhcm5pbmdzlF2UjAJ3MZRhaA+MIDIwMjYtMTAtMDFUMDk6MTQ6MDAuMTIzNDU2KzAwOjAwlIwLbW9kaWZpZWRfYXSUTmhyjC1odHRwczovL2xlbnouaW8vYXBpL3YxL3ZlcmlmaWNhdGlvbnMvNjhjYTRhYWaUaHSMLWh0dHBzOi8vbGVuei5pby9jL3RoZS1lYXJ0aC1pcy1yb3VuZC02OGNhNGFhZpRoiE51aCl9lGgrj5QoaG5ocmjvaCBo/Gj1aHRo6mgLaO1og2hmaCVoD2hpaIZo+GjmaBNo82jjaIiQaC1OdWJoiU51aCl9lGgrj5QoaGZoxGh2aLVo3GiJaLhot5BoLU51YmixKYGUfZQoaAV9lChotUsBaGaMHlRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpRot05ouGi6KYGUfZQoaAV9lChoaYwFRmFsc2WUaCCMBGhpZ2iUaGyMDHZlcmlmaWNhdGlvbpRowYh1aCl9lGgrj5QoaCBoaWhsaMGQaC1OdWJoxGjGKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGhpjAVGYWxzZZRoIIwEaGlnaJRohU5ozU5ock5ozk5oz12UaNFOaIZOaIhOdWgpfZRoK4+UKGjRaM1ocmggaAtoaWjPaIVozmiGaIiQaC1OdWJodmh4KYGUfZQoaAV9lChofF2UjAd2ZXJkaWN0lGFof4wHcGxhbm5lZJR1aCl9lGgrj5QoaH9ofJBoLU51YmjcaN4pgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaOOMCWF2YWlsYWJsZZRobowINGQ0NGRmYWaUaOaMIDBiOTJiNDQzZWI2OWI5YzcxNDM0ZmRjNjRmMmNhOGJllGhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UaBOMAmVulGjqjAdwcml2YXRllGgljAhzdGFuZGFyZJRo7WjuaO9dlGhpjAVGYWxzZZRoIIwEaGlnaJRo80sCaIOMKkZpbmRpbmcgZm9yIFRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpRo9YwqU3VtbWFyeSBmb3IgVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulGiGjCVGaXhlZDogVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulGj4XZSMAncxlGFoD4wgMjAyNi0xMC0wMVQwOToxNDowMC4xMjM0NTYrMDA6MDCUaPxOaHKMLWh0dHBzOi8vbGVuei5pby9hcGkvdjEvdmVyaWZpY2F0aW9ucy80ZDQ0ZGZhZpRodIwtaHR0cHM6Ly9sZW56LmlvL2MvdGhlLWVhcnRoLWlzLXJvdW5kLTRkNDRkZmFmlGiITnVoKX2UaCuPlChobmhyaO9oIGj8aPVodGjqaAto7WiDaGZoJWgPaGlohmj4aOZoE2jzaONoiJBoLU51YmiJTnVoKX2UaCuPlChoZmjEaHZotWjcaIlouGi3kGgtTnViaLEpgZR9lChoBX2UKGi1SwJoZowfUGFyaXMgaXMgdGhlIGNhcGl0YWwgb2YgRnJhbmNlLpRot05ouGi6KYGUfZQoaAV9lChoaYwEVHJ1ZZRoIIwEaGlnaJRobIwKYXNzZXNzbWVudJRowYl1aCl9lGgrj5QoaCBoaWhsaMGQaC1OdWJoxGjGKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGhpjARUcnVllGggjARoaWdolGiFTmjNTmhyTmjOTmjPXZRo0U5ohk5oiE51aCl9lGgrj5QoaNFozWhyaCBoC2hpaM9ohWjOaIZoiJBoLU51Ymh2aHgpgZR9lChoBX2UKGh8XZRof4wMbm90X3NlbGVjdGVklHVoKX2UaCuPlChof2h8kGgtTnViaNxOaIlOdWgpfZRoK4+UKGhmaMRodmi1aNxoiWi4aLeQaC1OdWJljAljaXRhdGlvbnOUXZR1aCl9lGgrj5QoaKhoo2gRaAtoFWimaF5oLmisaFVoD2hRaK5oDWiqaBNoCWpYAQAAaEtoB2iIkGgtTnViLg==", + "review__get_completed_issues_full.json": "gASVyhUAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMCmluY29tcGxldGWUjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lIwbMjAyNi0xMC0wMVQxMDowNzowMC4xMjM0NTZalIwIbGFuZ3VhZ2WUjAJlbpSMBnBvbGljeZRoAIwQRXNjYWxhdGlvblBvbGljeZSTlCmBlH2UKGgFfZQojAh2ZXJkaWN0c5RdlCiMBUZhbHNllIwMTW9zdGx5IEZhbHNllIwFTWl4ZWSUZYwKY29uZmlkZW5jZZRdlIwDbG93lGGMD21heF9hc3Nlc3NtZW50c5RLFIwRbWF4X3ZlcmlmaWNhdGlvbnOUSwWMBWRlcHRolIwIc3RhbmRhcmSUjA1tYXhfY2l0YXRpb25zlEsAjA1zdWdnZXN0X2VkaXRzlIl1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgbaCVoIGgnaChoJGgjkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjA1SZXZpZXdTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NsYWltc19zZWxlY3RlZJRLBIwLY2xhaW1fbGltaXSUSxSME2NsYWltX2xpbWl0X3JlYWNoZWSUiYwPaW5wdXRfdHJ1bmNhdGVklImMC2Fzc2Vzc21lbnRzlGgAjBZSZXZpZXdBc3Nlc3NtZW50Q291bnRzlJOUKYGUfZQoaAV9lCiMCWNvbXBsZXRlZJRLA4wGZmFpbGVklEsBdWgpfZRoK4+UKGg+aD+QaC1OdWKMDXZlcmlmaWNhdGlvbnOUaACMGFJldmlld1ZlcmlmaWNhdGlvbkNvdW50c5STlCmBlH2UKGgFfZQojAdwbGFubmVklEsCaD5LAWg/SwF1aCl9lGgrj5QoaD5oP2hIkGgtTnVijAZpc3N1ZXOUSwKMD2NpdGF0aW9uc19mb3VuZJROjBJjaXRhdGlvbnNfc2VsZWN0ZWSUTowOY2l0YXRpb25fbGltaXSUTowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJROjA9jaXRhdGlvbl9jaGVja3OUTowPY2l0YXRpb25faXNzdWVzlEsAjBFjaXRhdGlvbnNfc2tpcHBlZJROdWgpfZRoK4+UKGg0aDhoN2hNaEJoUmhPaEtoNWhOaFFoUGhMaDaQaC1OdWKMB2NyZWRpdHOUaACMDVJldmlld0NyZWRpdHOUk5QpgZR9lChoBX2UjAdjaGFyZ2VklEsNc2gpfZRoK4+UKGhbkGgtTnVijBJwb2xsX2FmdGVyX3NlY29uZHOUTmhLXZQoaACMC1Jldmlld0lzc3VllJOUKYGUfZQoaAV9lCiMC2NsYWltX2luZGV4lEsAjAVjbGFpbZSMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjA52ZXJpZmllZF9jbGFpbZROjAd2ZXJkaWN0lIwFRmFsc2WUaCCMBGhpZ2iUjAZzb3VyY2WUjAx2ZXJpZmljYXRpb26UjA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSME3ZlcmlmaWNhdGlvbl9zdGF0dXOUjAljb21wbGV0ZWSUjBB2ZXJpZmljYXRpb25fdXJslIwtaHR0cHM6Ly9sZW56LmlvL2FwaS92MS92ZXJpZmljYXRpb25zLzY4Y2E0YWFmlIwDdXJslIwtaHR0cHM6Ly9sZW56LmlvL2MvdGhlLWVhcnRoLWlzLXJvdW5kLTY4Y2E0YWFmlIwKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAd2ZXJkaWN0lGGMC2Rpc3Bvc2l0aW9ulIwHcGxhbm5lZJR1aCl9lGgrj5QoaH9ofJBoLU51YowLa2V5X2ZpbmRpbmeUjDRGaW5kaW5nIGZvciBUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulIwJcmF0aW9uYWxllE6MEXN1Z2dlc3RlZF9yZXdyaXRllIwvRml4ZWQ6IFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAdmYWlsdXJllE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGhmaG5ocmggaGlocGh2aIVoiWh0aGxoaGhlaINohmiIkGgtTnViaGEpgZR9lChoBX2UKGhlSwJoZoweVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulGhoTmhpjAVGYWxzZZRoIIwEaGlnaJRobIwKYXNzZXNzbWVudJRobk5ocIwGZmFpbGVklGhyTmh0Tmh2aHgpgZR9lChoBX2UKGh8XZSMB3ZlcmRpY3SUYWh/jAdwbGFubmVklHVoKX2UaCuPlChof2h8kGgtTnViaINOaIVOaIZOaIhoAIwMRmFpbHVyZUJsb2NrlJOUKYGUfZQoaAV9lCiMDmZhaWx1cmVfcmVhc29ulIwOcmVzZWFyY2hfZW1wdHmUjA1mYWlsdXJlX2NsYXNzlIwVaW5zdWZmaWNpZW50X2V2aWRlbmNllIwJcmV0cnlhYmxllImMBGhpbnSUTowIZG9jc191cmyUjDFodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjaW5zdWZmaWNpZW50LWV2aWRlbmNllHVoKX2UaCuPlChopmihaKdoo2ilkGgtTnViaIlOdWgpfZRoK4+UKGhmaG5ocmggaGlocGh2aIVoiWh0aGxoaGhlaINohmiIkGgtTnViZYwIZmFpbHVyZXOUXZRoAIwNUmV2aWV3RmFpbHVyZZSTlCmBlH2UKGgFfZQoaGVLAWhmjCJXYXRlciBib2lscyBhdCAxMDAgQyBhdCBzZWEgbGV2ZWwulIwFc3RhZ2WUjAphc3Nlc3NtZW50lGiIaJ0pgZR9lChoBX2UKGihjBR1cHN0cmVhbV91bmF2YWlsYWJsZZRoo4wUdXBzdHJlYW1fdW5hdmFpbGFibGWUaKWIaKaMTkEgbW9kZWwgcHJvdmlkZXIgd2FzIHVuYXZhaWxhYmxlIGZvciB0aGlzIGl0ZW0uIFJldHJ5IGl0OyBub3RoaW5nIHdhcyBjaGFyZ2VkLpRop4wwaHR0cHM6Ly9sZW56LmlvL2RvY3MvZXJyb3JzI3Vwc3RyZWFtLXVuYXZhaWxhYmxllHVoKX2UaCuPlChopmihaKdoo2ilkGgtTnVidWgpfZRoK4+UKGhmaGVotWiIkGgtTnViYWhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaIhOjAZjbGFpbXOUXZQoaACMC1Jldmlld0NsYWltlJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAaGaMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKGhpjAVGYWxzZZRoIIwEaGlnaJRobIwMdmVyaWZpY2F0aW9ulIwIaXNfaXNzdWWUiHVoKX2UaCuPlChoIGhpaGxo3pBoLU51YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRoaYwMTW9zdGx5IEZhbHNllGggjAZtZWRpdW2UaIVOjAdkaXNzZW50lE5ock6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZRopk5ohk5oiE51aCl9lGgrj5QoaKZo6mhyaCBoC2hpaOxohWjraIZoiJBoLU51Ymh2aHgpgZR9lChoBX2UKGh8XZSMB3ZlcmRpY3SUYWh/jAdwbGFubmVklHVoKX2UaCuPlChof2h8kGgtTnVijAx2ZXJpZmljYXRpb26UaACMElJldmlld1ZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJSMDmNvbnRlbnRfc3RhdHVzlIwJYXZhaWxhYmxllGhujAg2OGNhNGFhZpSMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJRoZowoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpRoE4wCZW6UjAp2aXNpYmlsaXR5lIwHcHJpdmF0ZZRoJYwIc3RhbmRhcmSUjAZkb21haW6UjACUjAhlbnRpdGllc5RdlGhpjAVGYWxzZZRoIIwEaGlnaJSMCmxlbnpfc2NvcmWUSwJog4w0RmluZGluZyBmb3IgVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIw0U3VtbWFyeSBmb3IgVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpRohowvRml4ZWQ6IFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAh3YXJuaW5nc5RdlIwCdzGUYWgPjCAyMDI2LTEwLTAxVDA5OjE0OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE5ocowtaHR0cHM6Ly9sZW56LmlvL2FwaS92MS92ZXJpZmljYXRpb25zLzY4Y2E0YWFmlGh0jC1odHRwczovL2xlbnouaW8vYy90aGUtZWFydGgtaXMtcm91bmQtNjhjYTRhYWaUaIhOdWgpfZRoK4+UKGhuaHJqCwEAAGggahgBAABqEQEAAGh0agYBAABoC2oJAQAAaINoZmglaA9oaWiGahQBAABqAgEAAGgTag8BAABo/2iIkGgtTnViaIlOdWgpfZRoK4+UKGhmaOFodmjSaPhoiWjVaNSQaC1OdWJozimBlH2UKGgFfZQoaNJLAWhmjCJXYXRlciBib2lscyBhdCAxMDAgQyBhdCBzZWEgbGV2ZWwulGjUTmjVTmjhaOMpgZR9lChoBX2UKGgLjAZmYWlsZWSUaGlOaCBOaIVOaOpOaHJOaOuMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGjsXZRopk5ohk5oiGidKYGUfZQoaAV9lChooYwUdXBzdHJlYW1fdW5hdmFpbGFibGWUaKOMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGiliGimjE5BIG1vZGVsIHByb3ZpZGVyIHdhcyB1bmF2YWlsYWJsZSBmb3IgdGhpcyBpdGVtLiBSZXRyeSBpdDsgbm90aGluZyB3YXMgY2hhcmdlZC6UaKeMMGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyN1cHN0cmVhbS11bmF2YWlsYWJsZZR1aCl9lGgrj5QoaKZooWinaKNopZBoLU51YnVoKX2UaCuPlChopmjqaHJoIGgLaGlo7GiFaOtohmiIkGgtTnViaHZOaPhOaIlOdWgpfZRoK4+UKGhmaOFodmjSaPhoiWjVaNSQaC1OdWJozimBlH2UKGgFfZQoaNJLAmhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UaNROaNVo1ymBlH2UKGgFfZQoaGmMBUZhbHNllGggjARoaWdolGhsjAphc3Nlc3NtZW50lGjeiHVoKX2UaCuPlChoIGhpaGxo3pBoLU51YmjhaOMpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaGmMBUZhbHNllGggjARoaWdolGiFTmjqTmhyTmjrTmjsXZRopk5ohk5oiE51aCl9lGgrj5QoaKZo6mhyaCBoC2hpaOxohWjraIZoiJBoLU51Ymh2aHgpgZR9lChoBX2UKGh8XZSMB3ZlcmRpY3SUYWh/jAdwbGFubmVklHVoKX2UaCuPlChof2h8kGgtTnViaPho+imBlH2UKGgFfZQoaAuMBmZhaWxlZJRo/4wJYXZhaWxhYmxllGhuTmoCAQAAjCAwYjkyYjQ0M2ViNjliOWM3MTQzNGZkYzY0ZjJjYThiZZRoZk5oE05qBgEAAE5oJU5qCQEAAE5qCwEAAF2UaGlOaCBOag8BAABOaINOahEBAABOaIZOahQBAABdlGgPTmoYAQAATmhyTmh0TmiIaJ0pgZR9lChoBX2UKGihjA5yZXNlYXJjaF9lbXB0eZRoo4wVaW5zdWZmaWNpZW50X2V2aWRlbmNllGiliWimTminjDFodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjaW5zdWZmaWNpZW50LWV2aWRlbmNllHVoKX2UaCuPlChopmihaKdoo2ilkGgtTnVidWgpfZRoK4+UKGhuaHJqCwEAAGggahgBAABqEQEAAGh0agYBAABoC2oJAQAAaINoZmglaA9oaWiGahQBAABqAgEAAGgTag8BAABo/2iIkGgtTnViaIlOdWgpfZRoK4+UKGhmaOFodmjSaPhoiWjVaNSQaC1OdWJozimBlH2UKGgFfZQoaNJLA2hmjB9QYXJpcyBpcyB0aGUgY2FwaXRhbCBvZiBGcmFuY2UulGjUTmjVaNcpgZR9lChoBX2UKGhpjARUcnVllGggjARoaWdolGhsjAphc3Nlc3NtZW50lGjeiXVoKX2UaCuPlChoIGhpaGxo3pBoLU51YmjhaOMpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaGmMBFRydWWUaCCMBGhpZ2iUaIVOaOpOaHJOaOtOaOxdlGimTmiGTmiITnVoKX2UaCuPlChopmjqaHJoIGgLaGlo7GiFaOtohmiIkGgtTnViaHZoeCmBlH2UKGgFfZQoaHxdlGh/jAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGh/aHyQaC1OdWJo+E5oiU51aCl9lGgrj5QoaGZo4Wh2aNJo+GiJaNVo1JBoLU51YmWMCWNpdGF0aW9uc5RdlHVoKX2UaCuPlChoxWitaBFoC2gVaMNoXmguaMloVWgPaFFoy2gNaMdoE2gJaoUBAABoS2gHaIiQaC1OdWIu", + "review__get_completed_issues_view_issues.json": "gASVDAsAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjAZpc3N1ZXOUjAZzdGF0dXOUjAljb21wbGV0ZWSUjAdvdXRjb21llIwKaW5jb21wbGV0ZZSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLBYwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSwCMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklEsEjAtjbGFpbV9saW1pdJRLFIwTY2xhaW1fbGltaXRfcmVhY2hlZJSJjA9pbnB1dF90cnVuY2F0ZWSUiYwLYXNzZXNzbWVudHOUaACMFlJldmlld0Fzc2Vzc21lbnRDb3VudHOUk5QpgZR9lChoBX2UKIwJY29tcGxldGVklEsDjAZmYWlsZWSUSwF1aCl9lGgrj5QoaD5oP5BoLU51YowNdmVyaWZpY2F0aW9uc5RoAIwYUmV2aWV3VmVyaWZpY2F0aW9uQ291bnRzlJOUKYGUfZQoaAV9lCiMB3BsYW5uZWSUSwJoPksBaD9LAXVoKX2UaCuPlChoPmg/aEiQaC1OdWKMBmlzc3Vlc5RLAowPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJROjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhSaE9oS2g1aE5oUWhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSw1zaCl9lGgrj5QoaFuQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlChoAIwLUmV2aWV3SXNzdWWUk5QpgZR9lChoBX2UKIwLY2xhaW1faW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMDnZlcmlmaWVkX2NsYWltlE6MB3ZlcmRpY3SUjAVGYWxzZZRoIIwEaGlnaJSMBnNvdXJjZZSMDHZlcmlmaWNhdGlvbpSMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwTdmVyaWZpY2F0aW9uX3N0YXR1c5SMCWNvbXBsZXRlZJSMEHZlcmlmaWNhdGlvbl91cmyUjC1odHRwczovL2xlbnouaW8vYXBpL3YxL3ZlcmlmaWNhdGlvbnMvNjhjYTRhYWaUjAN1cmyUjC1odHRwczovL2xlbnouaW8vYy90aGUtZWFydGgtaXMtcm91bmQtNjhjYTRhYWaUjAplc2NhbGF0aW9ulGgAjApFc2NhbGF0aW9ulJOUKYGUfZQoaAV9lCiMDW1hdGNoZWRfcnVsZXOUXZSMB3ZlcmRpY3SUYYwLZGlzcG9zaXRpb26UjAdwbGFubmVklHVoKX2UaCuPlChof2h8kGgtTnVijAtrZXlfZmluZGluZ5SMNEZpbmRpbmcgZm9yIFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlyYXRpb25hbGWUTowRc3VnZ2VzdGVkX3Jld3JpdGWUjC9GaXhlZDogVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMB2ZhaWx1cmWUTowPc3VnZ2VzdGVkX2VkaXRzlE51aCl9lGgrj5QoaGZobmhyaCBoaWhwaHZohWiJaHRobGhoaGVog2iGaIiQaC1OdWJoYSmBlH2UKGgFfZQoaGVLAmhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UaGhOaGmMBUZhbHNllGggjARoaWdolGhsjAphc3Nlc3NtZW50lGhuTmhwjAZmYWlsZWSUaHJOaHROaHZoeCmBlH2UKGgFfZQoaHxdlIwHdmVyZGljdJRhaH+MB3BsYW5uZWSUdWgpfZRoK4+UKGh/aHyQaC1OdWJog05ohU5ohk5oiGgAjAxGYWlsdXJlQmxvY2uUk5QpgZR9lChoBX2UKIwOZmFpbHVyZV9yZWFzb26UjA5yZXNlYXJjaF9lbXB0eZSMDWZhaWx1cmVfY2xhc3OUjBVpbnN1ZmZpY2llbnRfZXZpZGVuY2WUjAlyZXRyeWFibGWUiYwEaGludJROjAhkb2NzX3VybJSMMWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnN1ZmZpY2llbnQtZXZpZGVuY2WUdWgpfZRoK4+UKGimaKFop2ijaKWQaC1OdWJoiU51aCl9lGgrj5QoaGZobmhyaCBoaWhwaHZohWiJaHRobGhoaGVog2iGaIiQaC1OdWJljAhmYWlsdXJlc5RdlGgAjA1SZXZpZXdGYWlsdXJllJOUKYGUfZQoaAV9lChoZUsBaGaMIldhdGVyIGJvaWxzIGF0IDEwMCBDIGF0IHNlYSBsZXZlbC6UjAVzdGFnZZSMCmFzc2Vzc21lbnSUaIhonSmBlH2UKGgFfZQoaKGMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGijjBR1cHN0cmVhbV91bmF2YWlsYWJsZZRopYhopoxOQSBtb2RlbCBwcm92aWRlciB3YXMgdW5hdmFpbGFibGUgZm9yIHRoaXMgaXRlbS4gUmV0cnkgaXQ7IG5vdGhpbmcgd2FzIGNoYXJnZWQulGinjDBodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjdXBzdHJlYW0tdW5hdmFpbGFibGWUdWgpfZRoK4+UKGimaKFop2ijaKWQaC1OdWJ1aCl9lGgrj5QoaGZoZWi1aIiQaC1OdWJhaFFdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMC21vcmVfY2xhaW1zlF2UjBRtb3JlX2NsYWltX2xvY2F0aW9uc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRoiE6MBmNsYWltc5RdlIwJY2l0YXRpb25zlF2UdWgpfZRoK4+UKGjFaK1oEWgLaBVow2heaC5oyWhVaA9oUWgNaMdoE2gJaEtoB2iIkGgtTnViLg==", + "review__get_completed_positions.json": "gASV0QkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwKMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwKMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoKX2UaCuPlChoW5BoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UjAdmYWlsdXJllE6MBmNsYWltc5RdlChoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5RdlGgAjAhQb3NpdGlvbpSTlCmBlH2UKGgFfZQojAVzdGFydJRLAIwDZW5klEsojAR0ZXh0lIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpR1aCl9lGgrj5QoaH1ofmh/kGgtTnViYYwGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKIwHdmVyZGljdJSMBFRydWWUaCCMBGhpZ2iUjAZzb3VyY2WUjAphc3Nlc3NtZW50lIwIaXNfaXNzdWWUiXVoKX2UaCuPlChoIGiJaIxojpBoLU51YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRoiYwEVHJ1ZZRoIIwEaGlnaJSMCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEHZlcmlmaWNhdGlvbl91cmyUTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROaGtOdWgpfZRoK4+UKGigaJtonGggaAtoiWieaJponWihaGuQaC1OdWKMCmVzY2FsYXRpb26UaACMCkVzY2FsYXRpb26Uk5QpgZR9lChoBX2UKIwNbWF0Y2hlZF9ydWxlc5RdlIwLZGlzcG9zaXRpb26UjAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGisaKqQaC1OdWKMDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKX2UaCuPlChodGiRaKRoc2iwaLFog2h2kGgtTnViaG8pgZR9lChoBX2UKGhzSwFodIwiV2F0ZXIgYm9pbHMgYXQgMTAwIEMgYXQgc2VhIGxldmVsLpRodl2UaHkpgZR9lChoBX2UKGh9SzdofktZaH+MIldhdGVyIGJvaWxzIGF0IDEwMCBDIGF0IHNlYSBsZXZlbC6UdWgpfZRoK4+UKGh9aH5of5BoLU51YmFog2iFKYGUfZQoaAV9lChoiYwEVHJ1ZZRoIIwEaGlnaJRojIwKYXNzZXNzbWVudJRojol1aCl9lGgrj5QoaCBoiWiMaI6QaC1OdWJokWiTKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGiJjARUcnVllGggjARoaWdolGiaTmibTmicTmidTmieXZRooE5ooU5oa051aCl9lGgrj5QoaKBom2icaCBoC2iJaJ5ommidaKFoa5BoLU51YmikaKYpgZR9lChoBX2UKGiqXZRorIwMbm90X3NlbGVjdGVklHVoKX2UaCuPlChorGiqkGgtTnViaLBOaLFOdWgpfZRoK4+UKGh0aJFopGhzaLBosWiDaHaQaC1OdWJljAljaXRhdGlvbnOUXZR1aCl9lGgrj5QoaGVoYGgRaAtoFWhjaF5oLmhpaFVoD2hRaGxoDWhnaBNoCWjZaEtoB2hrkGgtTnViLg==", + "review__get_completed_recovered_claim.json": "gASVuQcAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAXNoKX2UaCuPlChoW5BoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UjAdmYWlsdXJllE6MBmNsYWltc5RdlGgAjAtSZXZpZXdDbGFpbZSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwFY2xhaW2UjCxPcmJpdCBNb3RvcnMgc3RvY2sgaW5jcmVhc2VkIGJ5IDQwJSBpbiAyMDIyLpSMCXBvc2l0aW9uc5RdlIwGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKIwHdmVyZGljdJSMBFRydWWUaCCMBGhpZ2iUjAZzb3VyY2WUjAphc3Nlc3NtZW50lIwIaXNfaXNzdWWUiXVoKX2UaCuPlChoIGh+aIFog5BoLU51YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRofowEVHJ1ZZRoIIwEaGlnaJSMCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEHZlcmlmaWNhdGlvbl91cmyUTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROaGtOdWgpfZRoK4+UKGiVaJBokWggaAtofmiTaI9okmiWaGuQaC1OdWKMCmVzY2FsYXRpb26UaACMCkVzY2FsYXRpb26Uk5QpgZR9lChoBX2UKIwNbWF0Y2hlZF9ydWxlc5RdlIwLZGlzcG9zaXRpb26UjAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGihaJ+QaC1OdWKMDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKX2UaCuPlChodGiGaJloc2ilaKZoeGh2kGgtTnViYYwJY2l0YXRpb25zlF2UdWgpfZRoK4+UKGhlaGBoEWgLaBVoY2heaC5oaWhVaA9oUWhsaA1oZ2gTaAloqWhLaAdoa5BoLU51Yi4=", + "review__get_completed_with_untraced_claim.json": "gASV0QkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwKMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwKMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoKX2UaCuPlChoW5BoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UjAdmYWlsdXJllE6MBmNsYWltc5RdlChoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5RdlGgAjAhQb3NpdGlvbpSTlCmBlH2UKGgFfZQojAVzdGFydJRLAIwDZW5klEsojAR0ZXh0lIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpR1aCl9lGgrj5QoaH1ofmh/kGgtTnViYYwGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKIwHdmVyZGljdJSMBFRydWWUaCCMBGhpZ2iUjAZzb3VyY2WUjAphc3Nlc3NtZW50lIwIaXNfaXNzdWWUiXVoKX2UaCuPlChoIGiJaIxojpBoLU51YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRoiYwEVHJ1ZZRoIIwEaGlnaJSMCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEHZlcmlmaWNhdGlvbl91cmyUTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROaGtOdWgpfZRoK4+UKGigaJtonGggaAtoiWieaJponWihaGuQaC1OdWKMCmVzY2FsYXRpb26UaACMCkVzY2FsYXRpb26Uk5QpgZR9lChoBX2UKIwNbWF0Y2hlZF9ydWxlc5RdlIwLZGlzcG9zaXRpb26UjAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGisaKqQaC1OdWKMDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKX2UaCuPlChodGiRaKRoc2iwaLFog2h2kGgtTnViaG8pgZR9lChoBX2UKGhzSwFodIwiV2F0ZXIgYm9pbHMgYXQgMTAwIEMgYXQgc2VhIGxldmVsLpRodl2UaHkpgZR9lChoBX2UKGh9SzJofktUaH+MIldhdGVyIGJvaWxzIGF0IDEwMCBDIGF0IHNlYSBsZXZlbC6UdWgpfZRoK4+UKGh9aH5of5BoLU51YmFog2iFKYGUfZQoaAV9lChoiYwEVHJ1ZZRoIIwEaGlnaJRojIwKYXNzZXNzbWVudJRojol1aCl9lGgrj5QoaCBoiWiMaI6QaC1OdWJokWiTKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGiJjARUcnVllGggjARoaWdolGiaTmibTmicTmidTmieXZRooE5ooU5oa051aCl9lGgrj5QoaKBom2icaCBoC2iJaJ5ommidaKFoa5BoLU51YmikaKYpgZR9lChoBX2UKGiqXZRorIwMbm90X3NlbGVjdGVklHVoKX2UaCuPlChorGiqkGgtTnViaLBOaLFOdWgpfZRoK4+UKGh0aJFopGhzaLBosWiDaHaQaC1OdWJljAljaXRhdGlvbnOUXZR1aCl9lGgrj5QoaGVoYGgRaAtoFWhjaF5oLmhpaFVoD2hRaGxoDWhnaBNoCWjZaEtoB2hrkGgtTnViLg==", + "review__get_during_quick_check.json": "gASVaQYAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJYXNzZXNzaW5nlIwHb3V0Y29tZZROjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGWgjaB5oJWgmaCJoIZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwCMBmZhaWxlZJRLAHVoJ32UaCmPlChoPGg9kGgrTnVijA12ZXJpZmljYXRpb25zlE6MBmlzc3Vlc5RLAIwPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJROjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCd9lGgpj5QoaDJoNmg1aENoQGhIaEVoQWgzaERoR2hGaEJoNJBoK051YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwFzaCd9lGgpj5QoaFGQaCtOdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5RLCmhBXZSMCGZhaWx1cmVzlF2UaEddlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMC21vcmVfY2xhaW1zlF2UjBRtb3JlX2NsYWltX2xvY2F0aW9uc5RdlIwObW9yZV9jaXRhdGlvbnOUXZSMB2ZhaWx1cmWUTowGY2xhaW1zlF2UaACMC1Jldmlld0NsYWltlJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAjAVjbGFpbZSMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lE6MCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAdydW5uaW5nlIwHdmVyZGljdJROaB5OjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBB2ZXJpZmljYXRpb25fdXJslE6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTmhhTnVoJ32UaCmPlChofGh3aHhoHmgLaHVoemh2aHlofWhhkGgrTnVijAplc2NhbGF0aW9ulE6MDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoJ32UaCmPlChoamhuaIBoaWiBaIJobWhskGgrTnViYYwJY2l0YXRpb25zlF2UdWgnfZRoKY+UKGhbaFZoEGgLaBNoWWhUaCxoX2hLaA5oR2hiaA1oXWgRaAlohWhBaAdoYZBoK051Yi4=", + "review__get_failed_every_assessment_failed.json": "gASVfw0AAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwGZmFpbGVklIwHb3V0Y29tZZSMCmluY29tcGxldGWUjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lIwbMjAyNi0xMC0wMVQxMDowNzowMC4xMjM0NTZalIwIbGFuZ3VhZ2WUjAJlbpSMBnBvbGljeZRoAIwQRXNjYWxhdGlvblBvbGljeZSTlCmBlH2UKGgFfZQojAh2ZXJkaWN0c5RdlCiMBUZhbHNllIwMTW9zdGx5IEZhbHNllIwFTWl4ZWSUZYwKY29uZmlkZW5jZZRdlIwDbG93lGGMD21heF9hc3Nlc3NtZW50c5RLFIwRbWF4X3ZlcmlmaWNhdGlvbnOUSwWMBWRlcHRolIwIc3RhbmRhcmSUjA1tYXhfY2l0YXRpb25zlEsAjA1zdWdnZXN0X2VkaXRzlIl1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgbaCVoIGgnaChoJGgjkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjA1SZXZpZXdTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NsYWltc19zZWxlY3RlZJRLAowLY2xhaW1fbGltaXSUSxSME2NsYWltX2xpbWl0X3JlYWNoZWSUiYwPaW5wdXRfdHJ1bmNhdGVklImMC2Fzc2Vzc21lbnRzlGgAjBZSZXZpZXdBc3Nlc3NtZW50Q291bnRzlJOUKYGUfZQoaAV9lCiMCWNvbXBsZXRlZJRLAIwGZmFpbGVklEsCdWgpfZRoK4+UKGg+aD+QaC1OdWKMDXZlcmlmaWNhdGlvbnOUaACMGFJldmlld1ZlcmlmaWNhdGlvbkNvdW50c5STlCmBlH2UKGgFfZQojAdwbGFubmVklEsAaD5LAGg/SwB1aCl9lGgrj5QoaD5oP2hIkGgtTnVijAZpc3N1ZXOUSwCMD2NpdGF0aW9uc19mb3VuZJROjBJjaXRhdGlvbnNfc2VsZWN0ZWSUTowOY2l0YXRpb25fbGltaXSUTowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJROjA9jaXRhdGlvbl9jaGVja3OUTowPY2l0YXRpb25faXNzdWVzlEsAjBFjaXRhdGlvbnNfc2tpcHBlZJROdWgpfZRoK4+UKGg0aDhoN2hNaEJoUmhPaEtoNWhOaFFoUGhMaDaQaC1OdWKMB2NyZWRpdHOUaACMDVJldmlld0NyZWRpdHOUk5QpgZR9lChoBX2UjAdjaGFyZ2VklEsAc2gpfZRoK4+UKGhbkGgtTnVijBJwb2xsX2FmdGVyX3NlY29uZHOUTmhLXZSMCGZhaWx1cmVzlF2UKGgAjA1SZXZpZXdGYWlsdXJllJOUKYGUfZQoaAV9lCiMC2NsYWltX2luZGV4lEsAjAVjbGFpbZSMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAVzdGFnZZSMCmFzc2Vzc21lbnSUjAdmYWlsdXJllGgAjAxGYWlsdXJlQmxvY2uUk5QpgZR9lChoBX2UKIwOZmFpbHVyZV9yZWFzb26UjBR1cHN0cmVhbV91bmF2YWlsYWJsZZSMDWZhaWx1cmVfY2xhc3OUjBR1cHN0cmVhbV91bmF2YWlsYWJsZZSMCXJldHJ5YWJsZZSIjARoaW50lIxOQSBtb2RlbCBwcm92aWRlciB3YXMgdW5hdmFpbGFibGUgZm9yIHRoaXMgaXRlbS4gUmV0cnkgaXQ7IG5vdGhpbmcgd2FzIGNoYXJnZWQulIwIZG9jc191cmyUjDBodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjdXBzdHJlYW0tdW5hdmFpbGFibGWUdWgpfZRoK4+UKGh3aHJoeWh0aHaQaC1OdWJ1aCl9lGgrj5QoaGhoZ2hqaGyQaC1OdWJoYymBlH2UKGgFfZQoaGdLAWhojCJXYXRlciBib2lscyBhdCAxMDAgQyBhdCBzZWEgbGV2ZWwulGhqjAphc3Nlc3NtZW50lGhsaG4pgZR9lChoBX2UKGhyjBR1cHN0cmVhbV91bmF2YWlsYWJsZZRodIwUdXBzdHJlYW1fdW5hdmFpbGFibGWUaHaIaHeMTkEgbW9kZWwgcHJvdmlkZXIgd2FzIHVuYXZhaWxhYmxlIGZvciB0aGlzIGl0ZW0uIFJldHJ5IGl0OyBub3RoaW5nIHdhcyBjaGFyZ2VkLpRoeYwwaHR0cHM6Ly9sZW56LmlvL2RvY3MvZXJyb3JzI3Vwc3RyZWFtLXVuYXZhaWxhYmxllHVoKX2UaCuPlChod2hyaHlodGh2kGgtTnVidWgpfZRoK4+UKGhoaGdoamhskGgtTnViZWhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaGxobimBlH2UKGgFfZQoaHKMEWFzc2Vzc21lbnRfZmFpbGVklGh0jBR1cHN0cmVhbV91bmF2YWlsYWJsZZRodohod4xdTm8gY2xhaW0gY291bGQgYmUgYXNzZXNzZWQuIFJldHJ5IHRoZSByZXZpZXcgd2l0aCBhIG5ldyBJZGVtcG90ZW5jeS1LZXk7IG5vdGhpbmcgd2FzIGNoYXJnZWQulGh5jDBodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjdXBzdHJlYW0tdW5hdmFpbGFibGWUdWgpfZRoK4+UKGh3aHJoeWh0aHaQaC1OdWKMBmNsYWltc5RdlChoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwBoaIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUTowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMBmZhaWxlZJSMB3ZlcmRpY3SUTmggTowJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowQdmVyaWZpY2F0aW9uX3VybJROjAplcnJvcl9jb2RllIwUdXBzdHJlYW1fdW5hdmFpbGFibGWUjBFpZGVudGlmaWVkX2NsYWltc5RdlGh3TowRc3VnZ2VzdGVkX3Jld3JpdGWUTmhsaG4pgZR9lChoBX2UKGhyjBR1cHN0cmVhbV91bmF2YWlsYWJsZZRodIwUdXBzdHJlYW1fdW5hdmFpbGFibGWUaHaIaHeMTkEgbW9kZWwgcHJvdmlkZXIgd2FzIHVuYXZhaWxhYmxlIGZvciB0aGlzIGl0ZW0uIFJldHJ5IGl0OyBub3RoaW5nIHdhcyBjaGFyZ2VkLpRoeYwwaHR0cHM6Ly9sZW56LmlvL2RvY3MvZXJyb3JzI3Vwc3RyZWFtLXVuYXZhaWxhYmxllHVoKX2UaCuPlChod2hyaHlodGh2kGgtTnVidWgpfZRoK4+UKGh3aLVotmggaAtos2i5aLRot2i7aGyQaC1OdWKMCmVzY2FsYXRpb26UTowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGhoaKxox2ioaMhoyWiraKqQaC1OdWJopCmBlH2UKGgFfZQoaKhLAWhojCJXYXRlciBib2lscyBhdCAxMDAgQyBhdCBzZWEgbGV2ZWwulGiqTmirTmisaK4pgZR9lChoBX2UKGgLjAZmYWlsZWSUaLNOaCBOaLROaLVOaLZOaLeMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGi5XZRod05ou05obGhuKYGUfZQoaAV9lChocowUdXBzdHJlYW1fdW5hdmFpbGFibGWUaHSMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGh2iGh3jE5BIG1vZGVsIHByb3ZpZGVyIHdhcyB1bmF2YWlsYWJsZSBmb3IgdGhpcyBpdGVtLiBSZXRyeSBpdDsgbm90aGluZyB3YXMgY2hhcmdlZC6UaHmMMGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyN1cHN0cmVhbS11bmF2YWlsYWJsZZR1aCl9lGgrj5QoaHdocmh5aHRodpBoLU51YnVoKX2UaCuPlChod2i1aLZoIGgLaLNouWi0aLdou2hskGgtTnViaMdOaMhOaMlOdWgpfZRoK4+UKGhoaKxox2ioaMhoyWiraKqQaC1OdWJljAljaXRhdGlvbnOUXZR1aCl9lGgrj5QoaJJoYGgRaAtoFWiQaF5oLmiWaFVoD2hRaKFoDWiUaBNoCWjjaEtoB2hskGgtTnViLg==", + "review__get_failed_no_claim.json": "gASV3gUAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwGZmFpbGVklIwHb3V0Y29tZZSMCXVuY2hlY2tlZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLBYwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSwCMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklE6MC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklE6MD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5ROjA12ZXJpZmljYXRpb25zlE6MBmlzc3Vlc5RLAIwPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJROjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aDxoOWhBaD5oOmg1aD1oQGg/aDtoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwBzaCl9lGgrj5QoaEqQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaDpdlIwIZmFpbHVyZXOUXZRoQF2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUTowUbW9yZV9jbGFpbV9sb2NhdGlvbnOUTowObW9yZV9jaXRhdGlvbnOUTowHZmFpbHVyZZRoAIwMRmFpbHVyZUJsb2NrlJOUKYGUfZQoaAV9lCiMDmZhaWx1cmVfcmVhc29ulIwIbm9fY2xhaW2UjA1mYWlsdXJlX2NsYXNzlIwNaW52YWxpZF9pbnB1dJSMCXJldHJ5YWJsZZSJjARoaW50lIyKVGhlIHRleHQgaXMgYSBncmVldGluZywgbm90IGEgc3RhdGVtZW50IGFib3V0IHRoZSB3b3JsZC4gU2VuZCBvbmUgZmFjdHVhbCBjbGFpbSwgb3IgcnVuIHRoZSB0ZXh0IHRocm91Z2ggL2V4dHJhY3QgdG8gZW51bWVyYXRlIGl0cyBjbGFpbXMulIwIZG9jc191cmyUjClodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjaW52YWxpZC1pbnB1dJR1aCl9lGgrj5QoaGJoXWhkaF9oYZBoLU51YowGY2xhaW1zlF2UjAljaXRhdGlvbnOUXZR1aCl9lGgrj5QoaFRoT2gRaAtoFWhSaE1oLmhWaERoD2hAaGhoDWhVaBNoCWhqaDpoB2hXkGgtTnViLg==", + "review__get_failed_no_claim_untraced.json": "gASV5gUAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwGZmFpbGVklIwHb3V0Y29tZZSMCXVuY2hlY2tlZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLBYwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSwCMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklE6MC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklE6MD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5ROjA12ZXJpZmljYXRpb25zlE6MBmlzc3Vlc5RLAIwPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJROjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aDxoOWhBaD5oOmg1aD1oQGg/aDtoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwBzaCl9lGgrj5QoaEqQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaDpdlIwIZmFpbHVyZXOUXZRoQF2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUTowUbW9yZV9jbGFpbV9sb2NhdGlvbnOUTowObW9yZV9jaXRhdGlvbnOUTowHZmFpbHVyZZRoAIwMRmFpbHVyZUJsb2NrlJOUKYGUfZQoaAV9lCiMDmZhaWx1cmVfcmVhc29ulIwIbm9fY2xhaW2UjA1mYWlsdXJlX2NsYXNzlIwNaW52YWxpZF9pbnB1dJSMCXJldHJ5YWJsZZSJjARoaW50lIySTm9uZSBvZiB0aGUgY2xhaW1zIGZvdW5kIGluIHRoZSB0ZXh0IGNvdWxkIGJlIHRyYWNlZCBiYWNrIHRvIGl0LiBTZW5kIG9uZSBmYWN0dWFsIGNsYWltLCBvciBydW4gdGhlIHRleHQgdGhyb3VnaCAvZXh0cmFjdCB0byBlbnVtZXJhdGUgaXRzIGNsYWltcy6UjAhkb2NzX3VybJSMKWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnZhbGlkLWlucHV0lHVoKX2UaCuPlChoYmhdaGRoX2hhkGgtTnVijAZjbGFpbXOUXZSMCWNpdGF0aW9uc5RdlHVoKX2UaCuPlChoVGhPaBFoC2gVaFJoTWguaFZoRGgPaEBoaGgNaFVoE2gJaGpoOmgHaFeQaC1OdWIu", + "review__get_more_claims.json": "gASVnQoAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSwKMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwKMC2NsYWltX2xpbWl0lEsCjBNjbGFpbV9saW1pdF9yZWFjaGVklIiMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwKMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoKX2UaCuPlChoW5BoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIweVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulGGMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UaACMDUNsYWltTG9jYXRpb26Uk5QpgZR9lChoBX2UKIwFY2xhaW2UjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjAlwb3NpdGlvbnOUXZRoAIwIUG9zaXRpb26Uk5QpgZR9lChoBX2UKIwFc3RhcnSUS1uMA2VuZJRLeYwEdGV4dJSMHlRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpR1aCl9lGgrj5QoaHhoeWh6kGgtTnViYXVoKX2UaCuPlChob2hxkGgtTnViYYwObW9yZV9jaXRhdGlvbnOUXZSMB2ZhaWx1cmWUTowGY2xhaW1zlF2UKGgAjAtSZXZpZXdDbGFpbZSTlCmBlH2UKGgFfZQojAVpbmRleJRLAGhvjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulGhxXZRodCmBlH2UKGgFfZQoaHhLAGh5SyhoeowoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpR1aCl9lGgrj5QoaHhoeWh6kGgtTnViYYwGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKIwHdmVyZGljdJSMBFRydWWUaCCMBGhpZ2iUjAZzb3VyY2WUjAphc3Nlc3NtZW50lIwIaXNfaXNzdWWUiXVoKX2UaCuPlChoIGiZaJxonpBoLU51YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRomYwEVHJ1ZZRoIIwEaGlnaJSMCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEHZlcmlmaWNhdGlvbl91cmyUTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROaIJOdWgpfZRoK4+UKGiwaKtorGggaAtomWiuaKporWixaIKQaC1OdWKMCmVzY2FsYXRpb26UaACMCkVzY2FsYXRpb26Uk5QpgZR9lChoBX2UKIwNbWF0Y2hlZF9ydWxlc5RdlIwLZGlzcG9zaXRpb26UjAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGi8aLqQaC1OdWKMDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKX2UaCuPlChob2ihaLRoimjAaMFok2hxkGgtTnViaIYpgZR9lChoBX2UKGiKSwFob4wiV2F0ZXIgYm9pbHMgYXQgMTAwIEMgYXQgc2VhIGxldmVsLpRocV2UaHQpgZR9lChoBX2UKGh4SzJoeUtUaHqMIldhdGVyIGJvaWxzIGF0IDEwMCBDIGF0IHNlYSBsZXZlbC6UdWgpfZRoK4+UKGh4aHloepBoLU51YmFok2iVKYGUfZQoaAV9lChomYwEVHJ1ZZRoIIwEaGlnaJRonIwKYXNzZXNzbWVudJRonol1aCl9lGgrj5QoaCBomWicaJ6QaC1OdWJooWijKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGiZjARUcnVllGggjARoaWdolGiqTmirTmisTmitTmiuXZRosE5osU5ogk51aCl9lGgrj5QoaLBoq2isaCBoC2iZaK5oqmitaLFogpBoLU51Ymi0aLYpgZR9lChoBX2UKGi6XZRovIwMbm90X3NlbGVjdGVklHVoKX2UaCuPlChovGi6kGgtTnViaMBOaMFOdWgpfZRoK4+UKGhvaKFotGiKaMBowWiTaHGQaC1OdWJljAljaXRhdGlvbnOUXZR1aCl9lGgrj5QoaGVoYGgRaAtoFWhjaF5oLmiAaFVoD2hRaINoDWhoaBNoCWjpaEtoB2iCkGgtTnViLg==", + "review__get_nested_verification_modified_at_crosses_midnight_by_minutes.json": "gASVfAwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMDGlzc3Vlc19mb3VuZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLBYwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSwCMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklEsBjAtjbGFpbV9saW1pdJRLFIwTY2xhaW1fbGltaXRfcmVhY2hlZJSJjA9pbnB1dF90cnVuY2F0ZWSUiYwLYXNzZXNzbWVudHOUaACMFlJldmlld0Fzc2Vzc21lbnRDb3VudHOUk5QpgZR9lChoBX2UKIwJY29tcGxldGVklEsBjAZmYWlsZWSUSwB1aCl9lGgrj5QoaD5oP5BoLU51YowNdmVyaWZpY2F0aW9uc5RoAIwYUmV2aWV3VmVyaWZpY2F0aW9uQ291bnRzlJOUKYGUfZQoaAV9lCiMB3BsYW5uZWSUSwFoPksBaD9LAHVoKX2UaCuPlChoPmg/aEiQaC1OdWKMBmlzc3Vlc5RLAYwPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJROjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhSaE9oS2g1aE5oUWhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwtzaCl9lGgrj5QoaFuQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlGgAjAtSZXZpZXdJc3N1ZZSTlCmBlH2UKGgFfZQojAtjbGFpbV9pbmRleJRLAIwFY2xhaW2UjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjA52ZXJpZmllZF9jbGFpbZROjAd2ZXJkaWN0lIwFRmFsc2WUaCCMBGhpZ2iUjAZzb3VyY2WUjAx2ZXJpZmljYXRpb26UjA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSME3ZlcmlmaWNhdGlvbl9zdGF0dXOUjAljb21wbGV0ZWSUjBB2ZXJpZmljYXRpb25fdXJslIwtaHR0cHM6Ly9sZW56LmlvL2FwaS92MS92ZXJpZmljYXRpb25zLzY4Y2E0YWFmlIwDdXJslIwtaHR0cHM6Ly9sZW56LmlvL2MvdGhlLWVhcnRoLWlzLXJvdW5kLTY4Y2E0YWFmlIwKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAd2ZXJkaWN0lGGMC2Rpc3Bvc2l0aW9ulIwHcGxhbm5lZJR1aCl9lGgrj5QoaH9ofJBoLU51YowLa2V5X2ZpbmRpbmeUjCpGaW5kaW5nIGZvciBUaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjAlyYXRpb25hbGWUTowRc3VnZ2VzdGVkX3Jld3JpdGWUjCVGaXhlZDogVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulIwHZmFpbHVyZZROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKX2UaCuPlChoZmhuaHJoIGhpaHBodmiFaIlodGhsaGhoZWiDaIZoiJBoLU51YmGMCGZhaWx1cmVzlF2UaFFdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMC21vcmVfY2xhaW1zlF2UjBRtb3JlX2NsYWltX2xvY2F0aW9uc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRoiE6MBmNsYWltc5RdlGgAjAtSZXZpZXdDbGFpbZSTlCmBlH2UKGgFfZQojAVpbmRleJRLAGhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKGhpjAVGYWxzZZRoIIwEaGlnaJRobIwMdmVyaWZpY2F0aW9ulIwIaXNfaXNzdWWUiHVoKX2UaCuPlChoIGhpaGxoqpBoLU51YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRoaYwFRmFsc2WUaCCMBGhpZ2iUaIVOjAdkaXNzZW50lE5ock6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTmiGTmiITnVoKX2UaCuPlChoumi2aHJoIGgLaGlouGiFaLdohmiIkGgtTnViaHZoeCmBlH2UKGgFfZQoaHxdlIwHdmVyZGljdJRhaH+MB3BsYW5uZWSUdWgpfZRoK4+UKGh/aHyQaC1OdWKMDHZlcmlmaWNhdGlvbpRoAIwSUmV2aWV3VmVyaWZpY2F0aW9ulJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklIwOY29udGVudF9zdGF0dXOUjAlhdmFpbGFibGWUaG6MCDY4Y2E0YWFmlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklGhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UaBOMAmVulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUaCWMCHN0YW5kYXJklIwGZG9tYWlulIwAlIwIZW50aXRpZXOUXZRoaYwFRmFsc2WUaCCMBGhpZ2iUjApsZW56X3Njb3JllEsCaIOMKkZpbmRpbmcgZm9yIFRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwqU3VtbWFyeSBmb3IgVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulGiGjCVGaXhlZDogVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulIwId2FybmluZ3OUXZSMAncxlGFoD4wgMjAyNi0xMC0wMVQyMzo1NTowMC4xMjM0NTYrMDA6MDCUjAttb2RpZmllZF9hdJSMIDIwMjYtMTAtMDJUMDA6MDM6MDAuMTIzNDU2KzAwOjAwlGhyjC1odHRwczovL2xlbnouaW8vYXBpL3YxL3ZlcmlmaWNhdGlvbnMvNjhjYTRhYWaUaHSMLWh0dHBzOi8vbGVuei5pby9jL3RoZS1lYXJ0aC1pcy1yb3VuZC02OGNhNGFhZpRoiE51aCl9lGgrj5QoaG5ocmjYaCBo5WjeaHRo02gLaNZog2hmaCVoD2hpaIZo4WjPaBNo3GjMaIiQaC1OdWJoiU51aCl9lGgrj5QoaGZorWh2aJ5oxWiJaKFooJBoLU51YmGMCWNpdGF0aW9uc5RdlHVoKX2UaCuPlChokWiMaBFoC2gVaI9oXmguaJVoVWgPaFFol2gNaJNoE2gJaO1oS2gHaIiQaC1OdWIu", + "review__get_nested_verification_modified_at_same_day_hours_apart.json": "gASVWgwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMDGlzc3Vlc19mb3VuZJSMCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUjBsyMDI2LTEwLTAxVDEwOjA3OjAwLjEyMzQ1NlqUjAhsYW5ndWFnZZSMAmVulIwGcG9saWN5lGgAjBBFc2NhbGF0aW9uUG9saWN5lJOUKYGUfZQoaAV9lCiMCHZlcmRpY3RzlF2UKIwFRmFsc2WUjAxNb3N0bHkgRmFsc2WUjAVNaXhlZJRljApjb25maWRlbmNllF2UjANsb3eUYYwPbWF4X2Fzc2Vzc21lbnRzlEsUjBFtYXhfdmVyaWZpY2F0aW9uc5RLBYwFZGVwdGiUjAhzdGFuZGFyZJSMDW1heF9jaXRhdGlvbnOUSwCMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoJWggaCdoKGgkaCOQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklEsBjAtjbGFpbV9saW1pdJRLFIwTY2xhaW1fbGltaXRfcmVhY2hlZJSJjA9pbnB1dF90cnVuY2F0ZWSUiYwLYXNzZXNzbWVudHOUaACMFlJldmlld0Fzc2Vzc21lbnRDb3VudHOUk5QpgZR9lChoBX2UKIwJY29tcGxldGVklEsBjAZmYWlsZWSUSwB1aCl9lGgrj5QoaD5oP5BoLU51YowNdmVyaWZpY2F0aW9uc5RoAIwYUmV2aWV3VmVyaWZpY2F0aW9uQ291bnRzlJOUKYGUfZQoaAV9lCiMB3BsYW5uZWSUSwFoPksBaD9LAHVoKX2UaCuPlChoPmg/aEiQaC1OdWKMBmlzc3Vlc5RLAYwPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJROjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCl9lGgrj5QoaDRoOGg3aE1oQmhSaE9oS2g1aE5oUWhQaExoNpBoLU51YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwtzaCl9lGgrj5QoaFuQaC1OdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEtdlGgAjAtSZXZpZXdJc3N1ZZSTlCmBlH2UKGgFfZQojAtjbGFpbV9pbmRleJRLAIwFY2xhaW2UjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjA52ZXJpZmllZF9jbGFpbZROjAd2ZXJkaWN0lIwFRmFsc2WUaCCMBGhpZ2iUjAZzb3VyY2WUjAx2ZXJpZmljYXRpb26UjA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSME3ZlcmlmaWNhdGlvbl9zdGF0dXOUjAljb21wbGV0ZWSUjBB2ZXJpZmljYXRpb25fdXJslIwtaHR0cHM6Ly9sZW56LmlvL2FwaS92MS92ZXJpZmljYXRpb25zLzY4Y2E0YWFmlIwDdXJslIwtaHR0cHM6Ly9sZW56LmlvL2MvdGhlLWVhcnRoLWlzLXJvdW5kLTY4Y2E0YWFmlIwKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAd2ZXJkaWN0lGGMC2Rpc3Bvc2l0aW9ulIwHcGxhbm5lZJR1aCl9lGgrj5QoaH9ofJBoLU51YowLa2V5X2ZpbmRpbmeUjCpGaW5kaW5nIGZvciBUaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjAlyYXRpb25hbGWUTowRc3VnZ2VzdGVkX3Jld3JpdGWUjCVGaXhlZDogVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulIwHZmFpbHVyZZROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKX2UaCuPlChoZmhuaHJoIGhpaHBodmiFaIlodGhsaGhoZWiDaIZoiJBoLU51YmGMCGZhaWx1cmVzlF2UaFFdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMC21vcmVfY2xhaW1zlF2UjBRtb3JlX2NsYWltX2xvY2F0aW9uc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRoiE6MBmNsYWltc5RdlGgAjAtSZXZpZXdDbGFpbZSTlCmBlH2UKGgFfZQojAVpbmRleJRLAGhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKGhpjAVGYWxzZZRoIIwEaGlnaJRobIwMdmVyaWZpY2F0aW9ulIwIaXNfaXNzdWWUiHVoKX2UaCuPlChoIGhpaGxoqpBoLU51YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRoaYwFRmFsc2WUaCCMBGhpZ2iUaIVOjAdkaXNzZW50lE5ock6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTmiGTmiITnVoKX2UaCuPlChoumi2aHJoIGgLaGlouGiFaLdohmiIkGgtTnViaHZoeCmBlH2UKGgFfZQoaHxdlIwHdmVyZGljdJRhaH+MB3BsYW5uZWSUdWgpfZRoK4+UKGh/aHyQaC1OdWKMDHZlcmlmaWNhdGlvbpRoAIwSUmV2aWV3VmVyaWZpY2F0aW9ulJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklIwOY29udGVudF9zdGF0dXOUjAlhdmFpbGFibGWUaG6MCDY4Y2E0YWFmlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklGhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UaBOMAmVulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUaCWMCHN0YW5kYXJklIwGZG9tYWlulIwAlIwIZW50aXRpZXOUXZRoaYwFRmFsc2WUaCCMBGhpZ2iUjApsZW56X3Njb3JllEsCaIOMKkZpbmRpbmcgZm9yIFRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwqU3VtbWFyeSBmb3IgVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulGiGjCVGaXhlZDogVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulIwId2FybmluZ3OUXZSMAncxlGFoD4wgMjAyNi0xMC0wMVQwODowMDowMC4xMjM0NTYrMDA6MDCUjAttb2RpZmllZF9hdJROaHKMLWh0dHBzOi8vbGVuei5pby9hcGkvdjEvdmVyaWZpY2F0aW9ucy82OGNhNGFhZpRodIwtaHR0cHM6Ly9sZW56LmlvL2MvdGhlLWVhcnRoLWlzLXJvdW5kLTY4Y2E0YWFmlGiITnVoKX2UaCuPlChobmhyaNhoIGjlaN5odGjTaAto1miDaGZoJWgPaGlohmjhaM9oE2jcaMxoiJBoLU51YmiJTnVoKX2UaCuPlChoZmitaHZonmjFaIlooWigkGgtTnViYYwJY2l0YXRpb25zlF2UdWgpfZRoK4+UKGiRaIxoEWgLaBVoj2heaC5olWhVaA9oUWiXaA1ok2gTaAlo7GhLaAdoiJBoLU51Yi4=", + "review__get_policy_resolved.json": "gASVlwcAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZSMBUZhbHNllGGMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsCjAVkZXB0aJSMA2xvd5SMDW1heF9jaXRhdGlvbnOUSwCMDXN1Z2dlc3RfZWRpdHOUiXWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBtoI2geaCVoJmgiaCGQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMB3N1bW1hcnmUaACMDVJldmlld1N1bW1hcnmUk5QpgZR9lChoBX2UKIwPY2xhaW1zX3NlbGVjdGVklEsBjAtjbGFpbV9saW1pdJRLFIwTY2xhaW1fbGltaXRfcmVhY2hlZJSJjA9pbnB1dF90cnVuY2F0ZWSUiYwLYXNzZXNzbWVudHOUaACMFlJldmlld0Fzc2Vzc21lbnRDb3VudHOUk5QpgZR9lChoBX2UKIwJY29tcGxldGVklEsBjAZmYWlsZWSUSwB1aCd9lGgpj5QoaDxoPZBoK051YowNdmVyaWZpY2F0aW9uc5RoAIwYUmV2aWV3VmVyaWZpY2F0aW9uQ291bnRzlJOUKYGUfZQoaAV9lCiMB3BsYW5uZWSUSwBoPEsAaD1LAHVoJ32UaCmPlChoPGg9aEaQaCtOdWKMBmlzc3Vlc5RLAIwPY2l0YXRpb25zX2ZvdW5klE6MEmNpdGF0aW9uc19zZWxlY3RlZJROjA5jaXRhdGlvbl9saW1pdJROjBZjaXRhdGlvbl9saW1pdF9yZWFjaGVklE6MD2NpdGF0aW9uX2NoZWNrc5ROjA9jaXRhdGlvbl9pc3N1ZXOUSwCMEWNpdGF0aW9uc19za2lwcGVklE51aCd9lGgpj5QoaDJoNmg1aEtoQGhQaE1oSWgzaExoT2hOaEpoNJBoK051YowHY3JlZGl0c5RoAIwNUmV2aWV3Q3JlZGl0c5STlCmBlH2UKGgFfZSMB2NoYXJnZWSUSwFzaCd9lGgpj5QoaFmQaCtOdWKMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROaEldlIwIZmFpbHVyZXOUXZRoT12UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlIwHZmFpbHVyZZROjAZjbGFpbXOUXZRoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMCXBvc2l0aW9uc5ROjAZyZXN1bHSUaACMDFJldmlld1Jlc3VsdJSTlCmBlH2UKGgFfZQojAd2ZXJkaWN0lIwEVHJ1ZZRoHowEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSJdWgnfZRoKY+UKGgeaHtofmiAkGgrTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGh7jARUcnVllGgejARoaWdolIwJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowQdmVyaWZpY2F0aW9uX3VybJROjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE5oaU51aCd9lGgpj5QoaJJojWiOaB5oC2h7aJBojGiPaJNoaZBoK051YowKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAtkaXNwb3NpdGlvbpSMDG5vdF9zZWxlY3RlZJR1aCd9lGgpj5QoaJ5onJBoK051YowMdmVyaWZpY2F0aW9ulE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgnfZRoKY+UKGhyaINolmhxaKJoo2h1aHSQaCtOdWJhjAljaXRhdGlvbnOUXZR1aCd9lGgpj5QoaGNoXmgRaAtoFWhhaFxoLGhnaFNoD2hPaGpoDWhlaBNoCWimaEloB2hpkGgrTnViLg==", + "review__get_queued.json": "gASVbAQAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwGcXVldWVklIwHb3V0Y29tZZROjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGWgjaB5oJWgmaCJoIZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUTowLY2xhaW1fbGltaXSUSxSME2NsYWltX2xpbWl0X3JlYWNoZWSUTowPaW5wdXRfdHJ1bmNhdGVklImMC2Fzc2Vzc21lbnRzlE6MDXZlcmlmaWNhdGlvbnOUTowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoJ32UaCmPlChoMmg2aDVoOmg3aD9oPGg4aDNoO2g+aD1oOWg0kGgrTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAHNoJ32UaCmPlChoSJBoK051YowScG9sbF9hZnRlcl9zZWNvbmRzlEsKaDhdlIwIZmFpbHVyZXOUXZRoPl2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUTowUbW9yZV9jbGFpbV9sb2NhdGlvbnOUTowObW9yZV9jaXRhdGlvbnOUTowHZmFpbHVyZZROjAZjbGFpbXOUXZSMCWNpdGF0aW9uc5RdlHVoJ32UaCmPlChoUmhNaBBoC2gTaFBoS2gsaFRoQmgOaD5oVmgNaFNoEWgJaFhoOGgHaFWQaCtOdWIu", + "review__get_suggested_edits.json": "gASV2xEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJdmVyaWZ5aW5nlIwHb3V0Y29tZZROjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SIdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGWgjaB5oJWgmaCJoIZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwKMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwKMBmZhaWxlZJRLAHVoJ32UaCmPlChoPGg9kGgrTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAmg8SwFoPUsAdWgnfZRoKY+UKGg8aD1oRpBoK051YowGaXNzdWVzlEsCjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoJ32UaCmPlChoMmg2aDVoS2hAaFBoTWhJaDNoTGhPaE5oSmg0kGgrTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLDHNoJ32UaCmPlChoWZBoK051YowScG9sbF9hZnRlcl9zZWNvbmRzlEsPaEldlChoAIwLUmV2aWV3SXNzdWWUk5QpgZR9lChoBX2UKIwLY2xhaW1faW5kZXiUSwCMBWNsYWltlIweVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulIwOdmVyaWZpZWRfY2xhaW2UTowHdmVyZGljdJSMBUZhbHNllGgejARoaWdolIwGc291cmNllIwMdmVyaWZpY2F0aW9ulIwPdmVyaWZpY2F0aW9uX2lklIwINjhjYTRhYWaUjBN2ZXJpZmljYXRpb25fc3RhdHVzlIwJY29tcGxldGVklIwQdmVyaWZpY2F0aW9uX3VybJSMLWh0dHBzOi8vbGVuei5pby9hcGkvdjEvdmVyaWZpY2F0aW9ucy82OGNhNGFhZpSMA3VybJSMLWh0dHBzOi8vbGVuei5pby9jL3RoZS1lYXJ0aC1pcy1yb3VuZC02OGNhNGFhZpSMCmVzY2FsYXRpb26UaACMCkVzY2FsYXRpb26Uk5QpgZR9lChoBX2UKIwNbWF0Y2hlZF9ydWxlc5RdlIwHdmVyZGljdJRhjAtkaXNwb3NpdGlvbpSMB3BsYW5uZWSUdWgnfZRoKY+UKGh9aHqQaCtOdWKMC2tleV9maW5kaW5nlIwqRmluZGluZyBmb3IgVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulIwJcmF0aW9uYWxllE6MEXN1Z2dlc3RlZF9yZXdyaXRllIwlRml4ZWQ6IFRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpSMB2ZhaWx1cmWUTowPc3VnZ2VzdGVkX2VkaXRzlGgAjA5TdWdnZXN0ZWRFZGl0c5STlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJSMBWVkaXRzlF2UaACMDVN1Z2dlc3RlZEVkaXSUk5QpgZR9lChoBX2UKIwIcG9zaXRpb26USwCMBXN0YXJ0lEsXjANlbmSUSx2MBHRleHSUjAZCZXJsaW6UjAtyZXBsYWNlbWVudJSMBVBhcmlzlHVoJ32UaCmPlCholmiVaJpol2iYkGgrTnViYXVoJ32UaCmPlChojmgLkGgrTnVidWgnfZRoKY+UKGhkaGxocGgeaGdobmh0aINoh2hyaGpoZmhjaIFohGiGkGgrTnViaF8pgZR9lChoBX2UKGhjSwFoZIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpRoZk5oZ4wFRmFsc2WUaB6MBGhpZ2iUaGqMCmFzc2Vzc21lbnSUaGxOaG6MCnByb2Nlc3NpbmeUaHBOaHJOaHRodimBlH2UKGgFfZQoaHpdlIwHdmVyZGljdJRhaH2MB3BsYW5uZWSUdWgnfZRoKY+UKGh9aHqQaCtOdWJogU5og05ohE5ohk5oh2iJKYGUfZQoaAV9lChoC4wHcGVuZGluZ5Rojk51aCd9lGgpj5QoaI5oC5BoK051YnVoJ32UaCmPlChoZGhsaHBoHmhnaG5odGiDaIdocmhqaGZoY2iBaIRohpBoK051YmWMCGZhaWx1cmVzlF2UaE9dlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMC21vcmVfY2xhaW1zlF2UjBRtb3JlX2NsYWltX2xvY2F0aW9uc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRohk6MBmNsYWltc5RdlChoAIwLUmV2aWV3Q2xhaW2Uk5QpgZR9lChoBX2UKIwFaW5kZXiUSwBoZIweVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulIwJcG9zaXRpb25zlF2UaACMCFBvc2l0aW9ulJOUKYGUfZQoaAV9lCholksAaJdLHmiYjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UdWgnfZRoKY+UKGiWaJdomJBoK051YmGMBnJlc3VsdJRoAIwMUmV2aWV3UmVzdWx0lJOUKYGUfZQoaAV9lChoZ4wFRmFsc2WUaB6MBGhpZ2iUaGqMDHZlcmlmaWNhdGlvbpSMCGlzX2lzc3VllIh1aCd9lGgpj5QoaB5oZ2hqaOGQaCtOdWKMCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaGeMBUZhbHNllGgejARoaWdolGiDTowHZGlzc2VudJROaHBOjAplcnJvcl9jb2RllE6MEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE5ohE5ohk51aCd9lGgpj5QoaPFo7WhwaB5oC2hnaO9og2juaIRohpBoK051Ymh0aHYpgZR9lChoBX2UKGh6XZSMB3ZlcmRpY3SUYWh9jAdwbGFubmVklHVoJ32UaCmPlChofWh6kGgrTnVijAx2ZXJpZmljYXRpb26UaACMElJldmlld1ZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJSMDmNvbnRlbnRfc3RhdHVzlIwJYXZhaWxhYmxllGhsjAg2OGNhNGFhZpSMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJRoZIweVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulGgRjAJlbpSMCnZpc2liaWxpdHmUjAdwcml2YXRllGgjjAhzdGFuZGFyZJSMBmRvbWFpbpSMAJSMCGVudGl0aWVzlF2UaGeMBUZhbHNllGgejARoaWdolIwKbGVuel9zY29yZZRLAmiBjCpGaW5kaW5nIGZvciBUaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjBFleGVjdXRpdmVfc3VtbWFyeZSMKlN1bW1hcnkgZm9yIFRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpRohIwlRml4ZWQ6IFRoZSBFaWZmZWwgVG93ZXIgaXMgaW4gQmVybGluLpSMCHdhcm5pbmdzlF2UjAJ3MZRhaA6MIDIwMjYtMTAtMDFUMDk6MTQ6MDAuMTIzNDU2KzAwOjAwlIwLbW9kaWZpZWRfYXSUTmhwjC1odHRwczovL2xlbnouaW8vYXBpL3YxL3ZlcmlmaWNhdGlvbnMvNjhjYTRhYWaUaHKMLWh0dHBzOi8vbGVuei5pby9jL3RoZS1lYXJ0aC1pcy1yb3VuZC02OGNhNGFhZpRohk51aCd9lGgpj5QoaGxocGoPAQAAaB5qHAEAAGoVAQAAaHJqCgEAAGgLag0BAABogWhkaCNoDmhnaIRqGAEAAGoGAQAAaBFqEwEAAGoDAQAAaIaQaCtOdWJoh2iJKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGiOXZRokSmBlH2UKGgFfZQoaJVLAGiWSxdol0sdaJiMBkJlcmxpbpRomowFUGFyaXOUdWgnfZRoKY+UKGiWaJVommiXaJiQaCtOdWJhdWgnfZRoKY+UKGiOaAuQaCtOdWJ1aCd9lGgpj5QoaGRo5Gh0aMxo/GiHaNhozpBoK051YmjIKYGUfZQoaAV9lChozEsBaGSMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UaM5dlGjRKYGUfZQoaAV9lCholkstaJdLVWiYjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulHVoJ32UaCmPlCholmiXaJiQaCtOdWJhaNho2imBlH2UKGgFfZQoaGeMBUZhbHNllGgejARoaWdolGhqjAphc3Nlc3NtZW50lGjhiHVoJ32UaCmPlChoHmhnaGpo4ZBoK051YmjkaOYpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaGeMBUZhbHNllGgejARoaWdolGiDTmjtTmhwTmjuTmjvXZRo8U5ohE5ohk51aCd9lGgpj5QoaPFo7WhwaB5oC2hnaO9og2juaIRohpBoK051Ymh0aHYpgZR9lChoBX2UKGh6XZSMB3ZlcmRpY3SUYWh9jAdwbGFubmVklHVoJ32UaCmPlChofWh6kGgrTnViaPxo/imBlH2UKGgFfZQoaAuMCnByb2Nlc3NpbmeUagMBAACMCWF2YWlsYWJsZZRobE5qBgEAAE5oZE5oEU5qCgEAAE5oI05qDQEAAE5qDwEAAF2UaGdOaB5OahMBAABOaIFOahUBAABOaIROahgBAABdlGgOTmocAQAATmhwTmhyTmiGTnVoJ32UaCmPlChobGhwag8BAABoHmocAQAAahUBAABocmoKAQAAaAtqDQEAAGiBaGRoI2gOaGdohGoYAQAAagYBAABoEWoTAQAAagMBAABohpBoK051YmiHaIkpgZR9lChoBX2UKGgLjAdwZW5kaW5nlGiOTnVoJ32UaCmPlChojmgLkGgrTnVidWgnfZRoKY+UKGhkaORodGjMaPxoh2jYaM6QaCtOdWJljAljaXRhdGlvbnOUXZR1aCd9lGgpj5QoaL9oumgQaAtoE2i9aFxoLGjDaFNoDmhPaMVoDWjBaBFoCWpmAQAAaEloB2iGkGgrTnViLg==", + "review__get_suggested_edits_issues_view.json": "gASVmgkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjAZpc3N1ZXOUjAZzdGF0dXOUjAl2ZXJpZnlpbmeUjAdvdXRjb21llE6MCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUTowIbGFuZ3VhZ2WUjAJlbpSMBnBvbGljeZRoAIwQRXNjYWxhdGlvblBvbGljeZSTlCmBlH2UKGgFfZQojAh2ZXJkaWN0c5RdlCiMBUZhbHNllIwMTW9zdGx5IEZhbHNllIwFTWl4ZWSUZYwKY29uZmlkZW5jZZRdlIwDbG93lGGMD21heF9hc3Nlc3NtZW50c5RLFIwRbWF4X3ZlcmlmaWNhdGlvbnOUSwWMBWRlcHRolIwIc3RhbmRhcmSUjA1tYXhfY2l0YXRpb25zlEsAjA1zdWdnZXN0X2VkaXRzlIh1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgZaCNoHmglaCZoImghkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjA1SZXZpZXdTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NsYWltc19zZWxlY3RlZJRLAowLY2xhaW1fbGltaXSUSxSME2NsYWltX2xpbWl0X3JlYWNoZWSUiYwPaW5wdXRfdHJ1bmNhdGVklImMC2Fzc2Vzc21lbnRzlGgAjBZSZXZpZXdBc3Nlc3NtZW50Q291bnRzlJOUKYGUfZQoaAV9lCiMCWNvbXBsZXRlZJRLAowGZmFpbGVklEsAdWgnfZRoKY+UKGg8aD2QaCtOdWKMDXZlcmlmaWNhdGlvbnOUaACMGFJldmlld1ZlcmlmaWNhdGlvbkNvdW50c5STlCmBlH2UKGgFfZQojAdwbGFubmVklEsCaDxLAWg9SwB1aCd9lGgpj5QoaDxoPWhGkGgrTnVijAZpc3N1ZXOUSwKMD2NpdGF0aW9uc19mb3VuZJROjBJjaXRhdGlvbnNfc2VsZWN0ZWSUTowOY2l0YXRpb25fbGltaXSUTowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJROjA9jaXRhdGlvbl9jaGVja3OUTowPY2l0YXRpb25faXNzdWVzlEsAjBFjaXRhdGlvbnNfc2tpcHBlZJROdWgnfZRoKY+UKGgyaDZoNWhLaEBoUGhNaEloM2hMaE9oTmhKaDSQaCtOdWKMB2NyZWRpdHOUaACMDVJldmlld0NyZWRpdHOUk5QpgZR9lChoBX2UjAdjaGFyZ2VklEsMc2gnfZRoKY+UKGhZkGgrTnVijBJwb2xsX2FmdGVyX3NlY29uZHOUSw9oSV2UKGgAjAtSZXZpZXdJc3N1ZZSTlCmBlH2UKGgFfZQojAtjbGFpbV9pbmRleJRLAIwFY2xhaW2UjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjA52ZXJpZmllZF9jbGFpbZROjAd2ZXJkaWN0lIwFRmFsc2WUaB6MBGhpZ2iUjAZzb3VyY2WUjAx2ZXJpZmljYXRpb26UjA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSME3ZlcmlmaWNhdGlvbl9zdGF0dXOUjAljb21wbGV0ZWSUjBB2ZXJpZmljYXRpb25fdXJslIwtaHR0cHM6Ly9sZW56LmlvL2FwaS92MS92ZXJpZmljYXRpb25zLzY4Y2E0YWFmlIwDdXJslIwtaHR0cHM6Ly9sZW56LmlvL2MvdGhlLWVhcnRoLWlzLXJvdW5kLTY4Y2E0YWFmlIwKZXNjYWxhdGlvbpRoAIwKRXNjYWxhdGlvbpSTlCmBlH2UKGgFfZQojA1tYXRjaGVkX3J1bGVzlF2UjAd2ZXJkaWN0lGGMC2Rpc3Bvc2l0aW9ulIwHcGxhbm5lZJR1aCd9lGgpj5QoaH1oepBoK051YowLa2V5X2ZpbmRpbmeUjCpGaW5kaW5nIGZvciBUaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjAlyYXRpb25hbGWUTowRc3VnZ2VzdGVkX3Jld3JpdGWUjCVGaXhlZDogVGhlIEVpZmZlbCBUb3dlciBpcyBpbiBCZXJsaW4ulIwHZmFpbHVyZZROjA9zdWdnZXN0ZWRfZWRpdHOUaACMDlN1Z2dlc3RlZEVkaXRzlJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklIwFZWRpdHOUXZRoAIwNU3VnZ2VzdGVkRWRpdJSTlCmBlH2UKGgFfZQojAhwb3NpdGlvbpRLAIwFc3RhcnSUSxeMA2VuZJRLHYwEdGV4dJSMBkJlcmxpbpSMC3JlcGxhY2VtZW50lIwFUGFyaXOUdWgnfZRoKY+UKGiWaJVommiXaJiQaCtOdWJhdWgnfZRoKY+UKGiOaAuQaCtOdWJ1aCd9lGgpj5QoaGRobGhwaB5oZ2huaHRog2iHaHJoamhmaGNogWiEaIaQaCtOdWJoXymBlH2UKGgFfZQoaGNLAWhkjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulGhmTmhnjAVGYWxzZZRoHowEaGlnaJRoaowKYXNzZXNzbWVudJRobE5obowKcHJvY2Vzc2luZ5RocE5ock5odGh2KYGUfZQoaAV9lChoel2UjAd2ZXJkaWN0lGFofYwHcGxhbm5lZJR1aCd9lGgpj5QoaH1oepBoK051YmiBTmiDTmiETmiGTmiHaIkpgZR9lChoBX2UKGgLjAdwZW5kaW5nlGiOTnVoJ32UaCmPlChojmgLkGgrTnVidWgnfZRoKY+UKGhkaGxocGgeaGdobmh0aINoh2hyaGpoZmhjaIFohGiGkGgrTnViZYwIZmFpbHVyZXOUXZRoT12UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlGiGTowGY2xhaW1zlF2UjAljaXRhdGlvbnOUXZR1aCd9lGgpj5QoaL9oumgQaAtoE2i9aFxoLGjDaFNoDmhPaA1owWgRaAloSWgHaIaQaCtOdWIu", + "review__get_url_review.json": "gASVKAgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoG2glaCBoJ2goaCRoI5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwGMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwGMBmZhaWxlZJRLAHVoKX2UaCuPlChoPmg/kGgtTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAGg+SwBoP0sAdWgpfZRoK4+UKGg+aD9oSJBoLU51YowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKX2UaCuPlChoNGg4aDdoTWhCaFJoT2hLaDVoTmhRaFBoTGg2kGgtTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAXNoKX2UaCuPlChoW5BoLU51YowScG9sbF9hZnRlcl9zZWNvbmRzlE5oS12UjAhmYWlsdXJlc5RdlGhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UjAdmYWlsdXJllE6MBmNsYWltc5RdlGgAjAtSZXZpZXdDbGFpbZSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwFY2xhaW2UjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulIwJcG9zaXRpb25zlF2UaACMCFBvc2l0aW9ulJOUKYGUfZQoaAV9lCiMBXN0YXJ0lE6MA2VuZJROjAR0ZXh0lIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpR1aCl9lGgrj5QoaH1ofmh/kGgtTnViYYwGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKIwHdmVyZGljdJSMBFRydWWUaCCMBGhpZ2iUjAZzb3VyY2WUjAphc3Nlc3NtZW50lIwIaXNfaXNzdWWUiXVoKX2UaCuPlChoIGiJaIxojpBoLU51YowKYXNzZXNzbWVudJRoAIwQUmV2aWV3QXNzZXNzbWVudJSTlCmBlH2UKGgFfZQoaAuMCWNvbXBsZXRlZJRoiYwEVHJ1ZZRoIIwEaGlnaJSMCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEHZlcmlmaWNhdGlvbl91cmyUTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROaGtOdWgpfZRoK4+UKGigaJtonGggaAtoiWieaJponWihaGuQaC1OdWKMCmVzY2FsYXRpb26UaACMCkVzY2FsYXRpb26Uk5QpgZR9lChoBX2UKIwNbWF0Y2hlZF9ydWxlc5RdlIwLZGlzcG9zaXRpb26UjAxub3Rfc2VsZWN0ZWSUdWgpfZRoK4+UKGisaKqQaC1OdWKMDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKX2UaCuPlChodGiRaKRoc2iwaLFog2h2kGgtTnViYYwJY2l0YXRpb25zlF2UdWgpfZRoK4+UKGhlaGBoEWgLaBVoY2heaC5oaWhVaA9oUWhsaA1oZ2gTaAlotGhLaAdoa5BoLU51Yi4=", + "review__get_verifying.json": "gASVYgsAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJdmVyaWZ5aW5nlIwHb3V0Y29tZZROjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAZwb2xpY3mUaACMEEVzY2FsYXRpb25Qb2xpY3mUk5QpgZR9lChoBX2UKIwIdmVyZGljdHOUXZQojAVGYWxzZZSMDE1vc3RseSBGYWxzZZSMBU1peGVklGWMCmNvbmZpZGVuY2WUXZSMA2xvd5RhjA9tYXhfYXNzZXNzbWVudHOUSxSMEW1heF92ZXJpZmljYXRpb25zlEsFjAVkZXB0aJSMCHN0YW5kYXJklIwNbWF4X2NpdGF0aW9uc5RLAIwNc3VnZ2VzdF9lZGl0c5SJdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGWgjaB5oJWgmaCJoIZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YowHc3VtbWFyeZRoAIwNUmV2aWV3U3VtbWFyeZSTlCmBlH2UKGgFfZQojA9jbGFpbXNfc2VsZWN0ZWSUSwKMC2NsYWltX2xpbWl0lEsUjBNjbGFpbV9saW1pdF9yZWFjaGVklImMD2lucHV0X3RydW5jYXRlZJSJjAthc3Nlc3NtZW50c5RoAIwWUmV2aWV3QXNzZXNzbWVudENvdW50c5STlCmBlH2UKGgFfZQojAljb21wbGV0ZWSUSwKMBmZhaWxlZJRLAHVoJ32UaCmPlChoPGg9kGgrTnVijA12ZXJpZmljYXRpb25zlGgAjBhSZXZpZXdWZXJpZmljYXRpb25Db3VudHOUk5QpgZR9lChoBX2UKIwHcGxhbm5lZJRLAWg8SwBoPUsAdWgnfZRoKY+UKGg8aD1oRpBoK051YowGaXNzdWVzlEsBjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoJ32UaCmPlChoMmg2aDVoS2hAaFBoTWhJaDNoTGhPaE5oSmg0kGgrTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoJ32UaCmPlChoWZBoK051YowScG9sbF9hZnRlcl9zZWNvbmRzlEsPaEldlGgAjAtSZXZpZXdJc3N1ZZSTlCmBlH2UKGgFfZQojAtjbGFpbV9pbmRleJRLAIwFY2xhaW2UjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulIwOdmVyaWZpZWRfY2xhaW2UTowHdmVyZGljdJSMBUZhbHNllGgejARoaWdolIwGc291cmNllIwKYXNzZXNzbWVudJSMD3ZlcmlmaWNhdGlvbl9pZJROjBN2ZXJpZmljYXRpb25fc3RhdHVzlIwKcHJvY2Vzc2luZ5SMEHZlcmlmaWNhdGlvbl91cmyUTowDdXJslE6MCmVzY2FsYXRpb26UaACMCkVzY2FsYXRpb26Uk5QpgZR9lChoBX2UKIwNbWF0Y2hlZF9ydWxlc5RdlIwHdmVyZGljdJRhjAtkaXNwb3NpdGlvbpSMB3BsYW5uZWSUdWgnfZRoKY+UKGh6aHeQaCtOdWKMC2tleV9maW5kaW5nlE6MCXJhdGlvbmFsZZROjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdmYWlsdXJllE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgnfZRoKY+UKGhkaGxob2geaGdobWhxaH9ogmhwaGpoZmhjaH5ogGiBkGgrTnViYYwIZmFpbHVyZXOUXZRoT12UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlGiBTowGY2xhaW1zlF2UKGgAjAtSZXZpZXdDbGFpbZSTlCmBlH2UKGgFfZQojAVpbmRleJRLAGhkjChUaGUgRVUgQUkgQWN0IHRvb2sgZWZmZWN0IGluIE1hcmNoIDIwMjQulIwJcG9zaXRpb25zlE6MBnJlc3VsdJRoAIwMUmV2aWV3UmVzdWx0lJOUKYGUfZQoaAV9lChoZ4wFRmFsc2WUaB6MBGhpZ2iUaGqMCmFzc2Vzc21lbnSUjAhpc19pc3N1ZZSIdWgnfZRoKY+UKGgeaGdoamijkGgrTnVijAphc3Nlc3NtZW50lGgAjBBSZXZpZXdBc3Nlc3NtZW50lJOUKYGUfZQoaAV9lChoC4wJY29tcGxldGVklGhnjAVGYWxzZZRoHowEaGlnaJRof06MB2Rpc3NlbnSUTmhvTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROaIBOaIFOdWgnfZRoKY+UKGizaK9ob2geaAtoZ2ixaH9osGiAaIGQaCtOdWJocWhzKYGUfZQoaAV9lChod12UjAd2ZXJkaWN0lGFoeowHcGxhbm5lZJR1aCd9lGgpj5QoaHpod5BoK051YowMdmVyaWZpY2F0aW9ulGgAjBJSZXZpZXdWZXJpZmljYXRpb26Uk5QpgZR9lChoBX2UKGgLjApwcm9jZXNzaW5nlIwOY29udGVudF9zdGF0dXOUjAlhdmFpbGFibGWUaGxOjAd0YXNrX2lklE5oZE5oEU6MCnZpc2liaWxpdHmUTmgjTowGZG9tYWlulE6MCGVudGl0aWVzlF2UaGdOaB5OjApsZW56X3Njb3JllE5ofk6MEWV4ZWN1dGl2ZV9zdW1tYXJ5lE5ogE6MCHdhcm5pbmdzlF2UaA5OjAttb2RpZmllZF9hdJROaG9OaHBOaIFOdWgnfZRoKY+UKGhsaG9oymgeaNBozWhwaMhoC2jJaH5oZGgjaA5oZ2iAaM5ox2gRaMxoxWiBkGgrTnViaIJOdWgnfZRoKY+UKGhkaKZocWiXaL5ogmiaaJmQaCtOdWJokymBlH2UKGgFfZQoaJdLAWhkjCJXYXRlciBib2lscyBhdCAxMDAgQyBhdCBzZWEgbGV2ZWwulGiZTmiaaJwpgZR9lChoBX2UKGhnjARUcnVllGgejARoaWdolGhqjAphc3Nlc3NtZW50lGijiXVoJ32UaCmPlChoHmhnaGpoo5BoK051YmimaKgpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaGeMBFRydWWUaB6MBGhpZ2iUaH9OaK9OaG9OaLBOaLFdlGizTmiATmiBTnVoJ32UaCmPlChos2ivaG9oHmgLaGdosWh/aLBogGiBkGgrTnViaHFocymBlH2UKGgFfZQoaHddlGh6jAxub3Rfc2VsZWN0ZWSUdWgnfZRoKY+UKGh6aHeQaCtOdWJovk5ogk51aCd9lGgpj5QoaGRopmhxaJdovmiCaJpomZBoK051YmWMCWNpdGF0aW9uc5RdlHVoJ32UaCmPlChoimiFaBBoC2gTaIhoXGgsaI5oU2gOaE9okGgNaIxoEWgJaPNoSWgHaIGQaCtOdWIu", + "review__idempotent_replay_202.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "review__receipt_202.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "review__receipt_202_empty_webhook_url.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "review__receipt_202_webhook_url.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "review__replay_later_202.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "verify__batch_202.json": "gASVpAEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNQmF0Y2hBY2NlcHRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAhiYXRjaF9pZJSMEDdlYTljZGJkNDA2NGUzY2aUjAVpdGVtc5RdlChoAIwMVGFza0FjY2VwdGVklJOUKYGUfZQoaAV9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjBNUaGUgRWFydGggaXMgcm91bmQulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBJoEJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgMKYGUfZQoaAV9lChoEIwgMGI5MmI0NDNlYjY5YjljNzE0MzRmZGM2NGYyY2E4YmWUaBKMIVdhdGVyIGJvaWxzIGF0IDEwMEMgYXQgc2VhIGxldmVsLpR1aBR9lGgWj5QoaBJoEJBoGE51YmV1aBR9lGgWj5QoaAdoCZBoGE51Yi4=", + "verify__batch_partial_202.json": "gASVPgEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNQmF0Y2hBY2NlcHRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAhiYXRjaF9pZJSMEDdlYTljZGJkNDA2NGUzY2aUjAVpdGVtc5RdlGgAjAxUYXNrQWNjZXB0ZWSUk5QpgZR9lChoBX2UKIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwKY2xhaW1fdGV4dJSMDEZpcnN0IGNsYWltLpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgSaBCQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJhdWgUfZSMB3BhcnRpYWyUiHNoFo+UKGgHaBpoCZBoGE51Yi4=", + "verify__delete_200.json": "gASVrAAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjACUjApjbGFpbV90ZXh0lGgIdYwSX19weWRhbnRpY19leHRyYV9flH2UjAJva5SIc4wXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaAyQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "verify__idempotency_key_replay.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "verify__implicit_dedup_200.json": "gASV2wAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UjAZzdGF0dXOUjAZxdWV1ZWSUc4wXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "verify__implicit_repeat_replay.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "verify__list_200.json": "gASVcQMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlChoAIwUVmVyaWZpY2F0aW9uTGlzdEl0ZW2Uk5QpgZR9lChoBX2UKIwPdmVyaWZpY2F0aW9uX2lklIwINjhjYTRhYWaUjAVjbGFpbZSME1RoZSBFYXJ0aCBpcyByb3VuZC6UjAZkb21haW6UjAdTY2llbmNllIwIZW50aXRpZXOUXZSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjBFzdWdnZXN0ZWRfcmV3cml0ZZROjApjcmVhdGVkX2F0lIwgMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTYrMDA6MDCUjAttb2RpZmllZF9hdJROjAhsYW5ndWFnZZSMAmVulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBBoDmgUaBhoIGgSaBZoI2giaB1oGmgbaB+QjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoCimBlH2UKGgFfZQoaA6MCDRkNDRkZmFmlGgQjBNUaGUgRWFydGggaXMgcm91bmQulGgSjAdTY2llbmNllGgUXZRoFowEVHJ1ZZRoGIwEaGlnaJRoGksJaBuMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UaB2MFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UaB9OaCCMIDIwMjYtMTAtMDFUMDk6MTQ6MDAuMTIzNDU2KzAwOjAwlGgiTmgjjAJlbpR1aCV9lGgnj5QoaBBoDmgUaBhoIGgSaBZoI2giaB1oGmgbaB+QaClOdWJljAV0b3RhbJRLAowEcGFnZZRLAYwJcGFnZV9zaXpllEsUdWglfZRoJ4+UKGg6aDtoOWgHkGgpTnViLg==", + "verify__list_200_modified_at_crosses_midnight_by_minutes.json": "gASVkAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlGgAjBRWZXJpZmljYXRpb25MaXN0SXRlbZSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMCmxlbnpfc2NvcmWUSwmMC2tleV9maW5kaW5nlIwuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpSMEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDIzOjU1OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lIwgMjAyNi0xMC0wMlQwMDowMzowMC4xMjM0NTYrMDA6MDCUjAhsYW5ndWFnZZSMAmVulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBBoDmgUaBhoIGgSaBZoJGgiaB1oGmgbaB+QjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJhjAV0b3RhbJRLAYwEcGFnZZRLAYwJcGFnZV9zaXpllEsUdWgmfZRoKI+UKGgsaC1oK2gHkGgqTnViLg==", + "verify__list_200_modified_at_same_day_hours_apart.json": "gASVbgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlGgAjBRWZXJpZmljYXRpb25MaXN0SXRlbZSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMCmxlbnpfc2NvcmWUSwmMC2tleV9maW5kaW5nlIwuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpSMEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA4OjAwOjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoEGgOaBRoGGggaBJoFmgjaCJoHWgaaBtoH5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBXRvdGFslEsBjARwYWdllEsBjAlwYWdlX3NpemWUSxR1aCV9lGgnj5QoaCtoLGgqaAeQaClOdWIu", + "verify__list_clamped_200.json": "gASVvgAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlIwFdG90YWyUSwCMBHBhZ2WUSwGMCXBhZ2Vfc2l6ZZRLZHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaApoC2gJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "verify__list_empty_200.json": "gASVvgAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlIwFdG90YWyUSwCMBHBhZ2WUSwGMCXBhZ2Vfc2l6ZZRLFHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaApoC2gJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "verify__list_page2_200.json": "gASVbgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlGgAjBRWZXJpZmljYXRpb25MaXN0SXRlbZSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMCmxlbnpfc2NvcmWUSwmMC2tleV9maW5kaW5nlIwuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpSMEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoEGgOaBRoGGggaBJoFmgjaCJoHWgaaBtoH5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBXRvdGFslEsDjARwYWdllEsCjAlwYWdlX3NpemWUSwJ1aCV9lGgnj5QoaCtoLGgqaAeQaClOdWIu", + "verify__replay_later_202.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "verify__select_202.json": "gASVpAEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNQmF0Y2hBY2NlcHRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAhiYXRjaF9pZJSMEDdlYTljZGJkNDA2NGUzY2aUjAVpdGVtc5RdlChoAIwMVGFza0FjY2VwdGVklJOUKYGUfZQoaAV9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjBNUaGUgRWFydGggaXMgcm91bmQulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBJoEJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgMKYGUfZQoaAV9lChoEIwgMGI5MmI0NDNlYjY5YjljNzE0MzRmZGM2NGYyY2E4YmWUaBKMIVdhdGVyIGJvaWxzIGF0IDEwMEMgYXQgc2VhIGxldmVsLpR1aBR9lGgWj5QoaBJoEJBoGE51YmV1aBR9lGgWj5QoaAdoCZBoGE51Yi4=", + "verify__status_cancelled_after_a_while.json": "gASVNgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjApDYW5jZWxsZWQulIwOZmFpbHVyZV9yZWFzb26UjAljYW5jZWxsZWSUjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMCWNhbmNlbGxlZJSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMJWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNjYW5jZWxsZWSUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", + "verify__status_completed.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", + "verify__status_completed_after_a_while.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", + "verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json": "gASVCQoAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDIzOjU1OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lIwgMjAyNi0xMC0wMlQwMDowMzowMC4xMjM0NTYrMDA6MDCUjAhsYW5ndWFnZZSMAmVulIwIY292ZXJhZ2WUaACMCENvdmVyYWdllJOUKYGUfZQoaAV9lChoB4wJdW5jb3ZlcmVklIwHcmVhc29uc5RdlIwEcGxhbpRhjA5jZXJ0aWZpY2F0ZV9pZJROjA9jZXJ0aWZpY2F0ZV91cmyUTowFYXNfb2aUTowIY3VycmVuY3mUjANFVVKUjANjYXCUTRAnjAlhZ2dyZWdhdGWUSiChBwCMDXRlcm1zX3ZlcnNpb26UjAJ2MZR1aBl9lGgbj5QoaLpotWi5aLZoB2i3aLtotGixkGgdTnVidWgZfZRoG4+UKGgkaC5oPmimaENoKGgsaEFoJmgqaKRoPGg6aEhoRWioaKpoSWhmaECQaB1OdWKMBmNsYWltc5RdlIwKY2FuZGlkYXRlc5RdlIwOc2ltaWxhcl9jbGFpbXOUXZSMBWVycm9ylGgMjA5mYWlsdXJlX3JlYXNvbpRoDIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUaAyMCXJldHJ5YWJsZZROjAhkb2NzX3VybJRoDHVoGX2UaBuPlChoHmgHaAmQaB1OdWIu", + "verify__status_completed_after_a_while_modified_at_same_day_hours_apart.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA4OjAwOjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", + "verify__status_completed_modified_at_crosses_midnight_by_minutes.json": "gASVCQoAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDIzOjU1OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lIwgMjAyNi0xMC0wMlQwMDowMzowMC4xMjM0NTYrMDA6MDCUjAhsYW5ndWFnZZSMAmVulIwIY292ZXJhZ2WUaACMCENvdmVyYWdllJOUKYGUfZQoaAV9lChoB4wJdW5jb3ZlcmVklIwHcmVhc29uc5RdlIwEcGxhbpRhjA5jZXJ0aWZpY2F0ZV9pZJROjA9jZXJ0aWZpY2F0ZV91cmyUTowFYXNfb2aUTowIY3VycmVuY3mUjANFVVKUjANjYXCUTRAnjAlhZ2dyZWdhdGWUSiChBwCMDXRlcm1zX3ZlcnNpb26UjAJ2MZR1aBl9lGgbj5QoaLpotWi5aLZoB2i3aLtotGixkGgdTnVidWgZfZRoG4+UKGgkaC5oPmimaENoKGgsaEFoJmgqaKRoPGg6aEhoRWioaKpoSWhmaECQaB1OdWKMBmNsYWltc5RdlIwKY2FuZGlkYXRlc5RdlIwOc2ltaWxhcl9jbGFpbXOUXZSMBWVycm9ylGgMjA5mYWlsdXJlX3JlYXNvbpRoDIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUaAyMCXJldHJ5YWJsZZROjAhkb2NzX3VybJRoDHVoGX2UaBuPlChoHmgHaAmQaB1OdWIu", + "verify__status_completed_modified_at_same_day_hours_apart.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA4OjAwOjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", + "verify__status_failed.json": "gASVbAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjCNQaXBlbGluZSBzdG9wcGVkIGF0OiByZXNlYXJjaF9lbXB0eZSMDmZhaWx1cmVfcmVhc29ulIwOcmVzZWFyY2hfZW1wdHmUjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMFWluc3VmZmljaWVudF9ldmlkZW5jZZSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMMWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnN1ZmZpY2llbnQtZXZpZGVuY2WUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", + "verify__status_failed_after_a_while.json": "gASVVgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjCRQaXBlbGluZSBzdG9wcGVkOiBjb25jbHVzaW9uX2ZhaWxlZC6UjA5mYWlsdXJlX3JlYXNvbpSMEWNvbmNsdXNpb25fZmFpbGVklIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjAhpbnRlcm5hbJSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMJGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnRlcm5hbJR1aBl9lGgbj5QoaAloB2gnaC1oKmgsaCWQaB1OdWIu", + "verify__status_failed_after_a_while_framing.json": "gASVaAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjCFQaXBlbGluZSBzdG9wcGVkOiBmcmFtaW5nX2ZhaWxlZC6UjA5mYWlsdXJlX3JlYXNvbpSMDmZyYW1pbmdfZmFpbGVklIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjBR1cHN0cmVhbV91bmF2YWlsYWJsZZSMCXJldHJ5YWJsZZSIjAhkb2NzX3VybJSMMGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyN1cHN0cmVhbS11bmF2YWlsYWJsZZR1aBl9lGgbj5QoaAloB2gnaC1oKmgsaCWQaB1OdWIu", + "verify__status_failed_retryable.json": "gASVdAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjChQaXBlbGluZSBzdG9wcGVkIGF0OiBhZGp1ZGljYXRpb25fZmFpbGVklIwOZmFpbHVyZV9yZWFzb26UjBNhZGp1ZGljYXRpb25fZmFpbGVklIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjBR1cHN0cmVhbV91bmF2YWlsYWJsZZSMCXJldHJ5YWJsZZSIjAhkb2NzX3VybJSMMGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyN1cHN0cmVhbS11bmF2YWlsYWJsZZR1aBl9lGgbj5QoaAloB2gnaC1oKmgsaCWQaB1OdWIu", + "verify__status_needs_input.json": "gASVGAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAtuZWVkc19pbnB1dJSMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMBnJlYXNvbpSMC211bHRpX2NsYWltlIwEaGludJSMYFRoZSB0ZXh0IGhvbGRzIHNldmVyYWwgZGlzdGluY3QgY2xhaW1zLiBTZW5kIHRoZSBvbmVzIHRvIGNoZWNrIHRvIFBPU1QgL3ZlcmlmeS97dGFza19pZH0vc2VsZWN0LpSMCHByb2dyZXNzlGgAjAhQcm9ncmVzc5STlCmBlH2UKGgFfZQojARzdGVwlIwAlIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UKGgAjA5DYW5kaWRhdGVDbGFpbZSTlCmBlH2UKGgFfZQojAR0ZXh0lIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMBmRvbWFpbpSMB1NjaWVuY2WUdWgbfZRoHY+UKGgqaCiQaB9OdWJoJCmBlH2UKGgFfZQoaCiMIVdhdGVyIGJvaWxzIGF0IDEwMEMgYXQgc2VhIGxldmVsLpRoKowHU2NpZW5jZZR1aBt9lGgdj5QoaCpoKJBoH051YmWMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoFowOZmFpbHVyZV9yZWFzb26UaBaMDmZhaWx1cmVfZGV0YWlslGgWjA1mYWlsdXJlX2NsYXNzlGgWjAlyZXRyeWFibGWUTowIZG9jc191cmyUaBZ1aBt9lGgdj5QoaA1oC2gJaAdoIZBoH051Yi4=", + "verify__status_not_a_claim.json": "gASV3wIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lIyPVGhlIGlucHV0IGlzIGEgZ3JlZXRpbmcsIG5vdCBhIHN0YXRlbWVudCB0aGF0IGNhbiBiZSBjaGVja2VkLiBTZW5kIG9uZSBmYWN0dWFsIGNsYWltLCBvciBydW4gdGhlIHRleHQgdGhyb3VnaCAvZXh0cmFjdCB0byBlbnVtZXJhdGUgaXRzIGNsYWltcy6UjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjBdOb3QgYSB2ZXJpZmlhYmxlIGNsYWltLpSMDmZhaWx1cmVfcmVhc29ulIwLbm90X2FfY2xhaW2UjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMDWludmFsaWRfaW5wdXSUjAlyZXRyeWFibGWUiYwIZG9jc191cmyUjClodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjaW52YWxpZC1pbnB1dJR1aBp9lGgcj5QoaA1oCWgHaChoLmgraC1oJpBoHk51Yi4=", + "verify__status_not_a_claim_after_a_while.json": "gASVVAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjB5QaXBlbGluZSBzdG9wcGVkOiBub3RfYV9jbGFpbS6UjA5mYWlsdXJlX3JlYXNvbpSMC25vdF9hX2NsYWltlIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjA1pbnZhbGlkX2lucHV0lIwJcmV0cnlhYmxllImMCGRvY3NfdXJslIwpaHR0cHM6Ly9sZW56LmlvL2RvY3MvZXJyb3JzI2ludmFsaWQtaW5wdXSUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", + "verify__status_older_run_completed.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", + "verify__status_older_run_failed_crashed.json": "gASVOwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjBBQaXBlbGluZSBmYWlsZWQulIwOZmFpbHVyZV9yZWFzb26UjAp0YXNrX2Vycm9ylIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjAhpbnRlcm5hbJSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMJGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnRlcm5hbJR1aBl9lGgbj5QoaAloB2gnaC1oKmgsaCWQaB1OdWIu", + "verify__status_older_run_failed_insufficient_evidence.json": "gASVbAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjCNQaXBlbGluZSBzdG9wcGVkIGF0OiByZXNlYXJjaF9lbXB0eZSMDmZhaWx1cmVfcmVhc29ulIwOcmVzZWFyY2hfZW1wdHmUjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMFWluc3VmZmljaWVudF9ldmlkZW5jZZSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMMWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnN1ZmZpY2llbnQtZXZpZGVuY2WUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", + "verify__status_older_run_in_progress.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAhyZXNlYXJjaJSMBWluZGV4lEsCjAV0b3RhbJRLBYwPZWxhcHNlZF9zZWNvbmRzlEsBjBJwb2xsX2FmdGVyX3NlY29uZHOUSwV1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYaBdoFmgUaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBnJlc3VsdJROjAZjbGFpbXOUXZSMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoDIwOZmFpbHVyZV9yZWFzb26UaAyMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlGgMjAlyZXRyeWFibGWUTowIZG9jc191cmyUaAx1aBp9lGgcj5QoaA5oB2gJkGgeTnViLg==", + "verify__status_processing.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAhyZXNlYXJjaJSMBWluZGV4lEsCjAV0b3RhbJRLBYwPZWxhcHNlZF9zZWNvbmRzlEsBjBJwb2xsX2FmdGVyX3NlY29uZHOUSwV1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYaBdoFmgUaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBnJlc3VsdJROjAZjbGFpbXOUXZSMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoDIwOZmFpbHVyZV9yZWFzb26UaAyMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlGgMjAlyZXRyeWFibGWUTowIZG9jc191cmyUaAx1aBp9lGgcj5QoaA5oB2gJkGgeTnViLg==", + "verify__status_processing_after_a_while.json": "gASVCgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAxhZGp1ZGljYXRpb26UjAVpbmRleJRLBIwFdG90YWyUSwWMD2VsYXBzZWRfc2Vjb25kc5RLAYwScG9sbF9hZnRlcl9zZWNvbmRzlEsFdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgXaBZoFGgZkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgafZRoHI+UKGgOaAdoCZBoHk51Yi4=", + "verify__status_task_stuck.json": "gASVewIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjDhUaGUgdGFzayB3YXMgbmV2ZXIgY29tcGxldGVkIGFuZCBoYXMgYmVlbiBtYXJrZWQgZmFpbGVkLpSMDmZhaWx1cmVfcmVhc29ulIwKdGFza19zdHVja5SMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlIwUdXBzdHJlYW1fdW5hdmFpbGFibGWUjAlyZXRyeWFibGWUiIwIZG9jc191cmyUjDBodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjdXBzdHJlYW0tdW5hdmFpbGFibGWUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", + "verify__submit_202.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "verify__submit_202_options.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "verify__submit_202_text_alias.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "verify__verification_200.json": "gASVXAgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UaACMCUVudGl0eVJlZpSTlCmBlH2UKGgFfZQojARuYW1llIwFRWFydGiUjANxaWSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBhoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoG32UaB2PlChoNmg+aDpoOGg8kGgfTnViaDIpgZR9lChoBX2UKGg2jANFU0GUaDiMEEVhcnRoIGZyb20gc3BhY2WUaDqMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaDyMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaD6MCjIwMjUtMDEtMTaUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoK12UdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJoVymBlH2UKGgFfZQoaFuMClJldmlld2VyIEKUaF2MB1NvdXJjZXOUaF9HQCIAAAAAAABoYIwXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaCtdlIwNd2VhayBzb3VyY2UgQZRhdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoG32UaB2PlChod2h5aHWQaB9OdWKMCmRlYmF0ZV9jb26UaHEpgZR9lChoBX2UKGh1jAdTa2VwdGljlGh3jCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGh5jCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBt9lGgdj5QoaHdoeWh1kGgfTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgbfZRoHY+UKGhUaH1oUmiGaG+QaB9OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKIwGc3RhdHVzlIwJdW5jb3ZlcmVklIwHcmVhc29uc5RdlIwEcGxhbpRhjA5jZXJ0aWZpY2F0ZV9pZJROjA9jZXJ0aWZpY2F0ZV91cmyUTowFYXNfb2aUTowIY3VycmVuY3mUjANFVVKUjANjYXCUTRAnjAlhZ2dyZWdhdGWUSiChBwCMDXRlcm1zX3ZlcnNpb26UjAJ2MZR1aBt9lGgdj5QoaKBom2ifaJxolWidaKFommiXkGgfTnVidWgbfZRoHY+UKGgHaBFoJGiMaCloC2gPaCdoCWgNaIpoImggaC5oK2iNaI9oL2hMaCaQaB9OdWIu", + "verify__verification_200_covered.json": "gASVzQgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UaACMCUVudGl0eVJlZpSTlCmBlH2UKGgFfZQojARuYW1llIwFRWFydGiUjANxaWSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBhoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoG32UaB2PlChoNmg+aDpoOGg8kGgfTnViaDIpgZR9lChoBX2UKGg2jANFU0GUaDiMEEVhcnRoIGZyb20gc3BhY2WUaDqMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaDyMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaD6MCjIwMjUtMDEtMTaUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoK12UdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJoVymBlH2UKGgFfZQoaFuMClJldmlld2VyIEKUaF2MB1NvdXJjZXOUaF9HQCIAAAAAAABoYIwXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaCtdlIwNd2VhayBzb3VyY2UgQZRhdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoG32UaB2PlChod2h5aHWQaB9OdWKMCmRlYmF0ZV9jb26UaHEpgZR9lChoBX2UKGh1jAdTa2VwdGljlGh3jCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGh5jCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBt9lGgdj5QoaHdoeWh1kGgfTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgbfZRoHY+UKGhUaH1oUmiGaG+QaB9OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKIwGc3RhdHVzlIwHY292ZXJlZJSMB3JlYXNvbnOUXZSMDmNlcnRpZmljYXRlX2lklIwgOTViMGYzZGQwMDhiNDY4NDZiNmRkNmIxNTA5OWUyYjOUjA9jZXJ0aWZpY2F0ZV91cmyUjDxodHRwczovL2xlbnouaW8vY2VydGlmaWNhdGUvOTViMGYzZGQwMDhiNDY4NDZiNmRkNmIxNTA5OWUyYjOUjAVhc19vZpSMGTIwMjYtMDMtMTBUMTI6MDE6MzArMDA6MDCUjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoG32UaB2PlChoomibaKFonWiVaJ9oo2iZaJeQaB9OdWJ1aBt9lGgdj5QoaAdoEWgkaIxoKWgLaA9oJ2gJaA1oimgiaCBoLmgraI1oj2gvaExoJpBoH051Yi4=", + "verify__verification_200_modified_at_crosses_midnight_by_minutes.json": "gASVfggAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UaACMCUVudGl0eVJlZpSTlCmBlH2UKGgFfZQojARuYW1llIwFRWFydGiUjANxaWSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBhoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoG32UaB2PlChoNmg+aDpoOGg8kGgfTnViaDIpgZR9lChoBX2UKGg2jANFU0GUaDiMEEVhcnRoIGZyb20gc3BhY2WUaDqMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaDyMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaD6MCjIwMjUtMDEtMTaUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoK12UdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJoVymBlH2UKGgFfZQoaFuMClJldmlld2VyIEKUaF2MB1NvdXJjZXOUaF9HQCIAAAAAAABoYIwXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaCtdlIwNd2VhayBzb3VyY2UgQZRhdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoG32UaB2PlChod2h5aHWQaB9OdWKMCmRlYmF0ZV9jb26UaHEpgZR9lChoBX2UKGh1jAdTa2VwdGljlGh3jCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGh5jCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBt9lGgdj5QoaHdoeWh1kGgfTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgbfZRoHY+UKGhUaH1oUmiGaG+QaB9OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDIzOjU1OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lIwgMjAyNi0xMC0wMlQwMDowMzowMC4xMjM0NTYrMDA6MDCUjAhsYW5ndWFnZZSMAmVulIwIY292ZXJhZ2WUaACMCENvdmVyYWdllJOUKYGUfZQoaAV9lCiMBnN0YXR1c5SMCXVuY292ZXJlZJSMB3JlYXNvbnOUXZSMBHBsYW6UYYwOY2VydGlmaWNhdGVfaWSUTowPY2VydGlmaWNhdGVfdXJslE6MBWFzX29mlE6MCGN1cnJlbmN5lIwDRVVSlIwDY2FwlE0QJ4wJYWdncmVnYXRllEogoQcAjA10ZXJtc192ZXJzaW9ulIwCdjGUdWgbfZRoHY+UKGihaJxooGidaJZonmiiaJtomJBoH051YnVoG32UaB2PlChoB2gRaCRojGgpaAtoD2gnaAloDWiKaCJoIGguaCtojmiQaC9oTGgmkGgfTnViLg==", + "verify__verification_200_modified_at_null.json": "gASVXAgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UaACMCUVudGl0eVJlZpSTlCmBlH2UKGgFfZQojARuYW1llIwFRWFydGiUjANxaWSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBhoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoG32UaB2PlChoNmg+aDpoOGg8kGgfTnViaDIpgZR9lChoBX2UKGg2jANFU0GUaDiMEEVhcnRoIGZyb20gc3BhY2WUaDqMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaDyMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaD6MCjIwMjUtMDEtMTaUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoK12UdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJoVymBlH2UKGgFfZQoaFuMClJldmlld2VyIEKUaF2MB1NvdXJjZXOUaF9HQCIAAAAAAABoYIwXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaCtdlIwNd2VhayBzb3VyY2UgQZRhdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoG32UaB2PlChod2h5aHWQaB9OdWKMCmRlYmF0ZV9jb26UaHEpgZR9lChoBX2UKGh1jAdTa2VwdGljlGh3jCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGh5jCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBt9lGgdj5QoaHdoeWh1kGgfTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgbfZRoHY+UKGhUaH1oUmiGaG+QaB9OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKIwGc3RhdHVzlIwJdW5jb3ZlcmVklIwHcmVhc29uc5RdlIwEcGxhbpRhjA5jZXJ0aWZpY2F0ZV9pZJROjA9jZXJ0aWZpY2F0ZV91cmyUTowFYXNfb2aUTowIY3VycmVuY3mUjANFVVKUjANjYXCUTRAnjAlhZ2dyZWdhdGWUSiChBwCMDXRlcm1zX3ZlcnNpb26UjAJ2MZR1aBt9lGgdj5QoaKBom2ifaJxolWidaKFommiXkGgfTnVidWgbfZRoHY+UKGgHaBFoJGiMaCloC2gPaCdoCWgNaIpoImggaC5oK2iNaI9oL2hMaCaQaB9OdWIu", + "verify__verification_200_modified_at_same_day_hours_apart.json": "gASVXAgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UaACMCUVudGl0eVJlZpSTlCmBlH2UKGgFfZQojARuYW1llIwFRWFydGiUjANxaWSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBhoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoG32UaB2PlChoNmg+aDpoOGg8kGgfTnViaDIpgZR9lChoBX2UKGg2jANFU0GUaDiMEEVhcnRoIGZyb20gc3BhY2WUaDqMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaDyMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaD6MCjIwMjUtMDEtMTaUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoK12UdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJoVymBlH2UKGgFfZQoaFuMClJldmlld2VyIEKUaF2MB1NvdXJjZXOUaF9HQCIAAAAAAABoYIwXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaCtdlIwNd2VhayBzb3VyY2UgQZRhdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoG32UaB2PlChod2h5aHWQaB9OdWKMCmRlYmF0ZV9jb26UaHEpgZR9lChoBX2UKGh1jAdTa2VwdGljlGh3jCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGh5jCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBt9lGgdj5QoaHdoeWh1kGgfTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgbfZRoHY+UKGhUaH1oUmiGaG+QaB9OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA4OjAwOjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKIwGc3RhdHVzlIwJdW5jb3ZlcmVklIwHcmVhc29uc5RdlIwEcGxhbpRhjA5jZXJ0aWZpY2F0ZV9pZJROjA9jZXJ0aWZpY2F0ZV91cmyUTowFYXNfb2aUTowIY3VycmVuY3mUjANFVVKUjANjYXCUTRAnjAlhZ2dyZWdhdGWUSiChBwCMDXRlcm1zX3ZlcnNpb26UjAJ2MZR1aBt9lGgdj5QoaKBom2ifaJxolWidaKFommiXkGgfTnVidWgbfZRoHY+UKGgHaBFoJGiMaCloC2gPaCdoCWgNaIpoImggaC5oK2iNaI9oL2hMaCaQaB9OdWIu", + "verify__verification_200_modified_at_set.json": "gASVfggAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UaACMCUVudGl0eVJlZpSTlCmBlH2UKGgFfZQojARuYW1llIwFRWFydGiUjANxaWSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBhoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoG32UaB2PlChoNmg+aDpoOGg8kGgfTnViaDIpgZR9lChoBX2UKGg2jANFU0GUaDiMEEVhcnRoIGZyb20gc3BhY2WUaDqMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaDyMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaD6MCjIwMjUtMDEtMTaUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoK12UdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJoVymBlH2UKGgFfZQoaFuMClJldmlld2VyIEKUaF2MB1NvdXJjZXOUaF9HQCIAAAAAAABoYIwXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaCtdlIwNd2VhayBzb3VyY2UgQZRhdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoG32UaB2PlChod2h5aHWQaB9OdWKMCmRlYmF0ZV9jb26UaHEpgZR9lChoBX2UKGh1jAdTa2VwdGljlGh3jCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGh5jCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBt9lGgdj5QoaHdoeWh1kGgfTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgbfZRoHY+UKGhUaH1oUmiGaG+QaB9OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDIzOjU1OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lIwgMjAyNi0xMC0wMlQwMDowMzowMC4xMjM0NTYrMDA6MDCUjAhsYW5ndWFnZZSMAmVulIwIY292ZXJhZ2WUaACMCENvdmVyYWdllJOUKYGUfZQoaAV9lCiMBnN0YXR1c5SMCXVuY292ZXJlZJSMB3JlYXNvbnOUXZSMBHBsYW6UYYwOY2VydGlmaWNhdGVfaWSUTowPY2VydGlmaWNhdGVfdXJslE6MBWFzX29mlE6MCGN1cnJlbmN5lIwDRVVSlIwDY2FwlE0QJ4wJYWdncmVnYXRllEogoQcAjA10ZXJtc192ZXJzaW9ulIwCdjGUdWgbfZRoHY+UKGihaJxooGidaJZonmiiaJtomJBoH051YnVoG32UaB2PlChoB2gRaCRojGgpaAtoD2gnaAloDWiKaCJoIGguaCtojmiQaC9oTGgmkGgfTnViLg==", + "verify__verification_200_result_json_conclusion_only.json": "gASVXgQAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UjA9wcmVzdW1lZF9pbnRlbnSUjACUjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwKbGVuel9zY29yZZRLCYwLa2V5X2ZpbmRpbmeUjC5UaGUgRWFydGggaXMgYXBwcm94aW1hdGVseSBzcGhlcmljYWwgaW4gc2hhcGUulIwRZXhlY3V0aXZlX3N1bW1hcnmUjBZUaGUgY2xhaW0gaXMgdmVyaWZpZWQulIwId2FybmluZ3OUXZSMEXN1Z2dlc3RlZF9yZXdyaXRllE6MB3NvdXJjZXOUXZSMBWF1ZGl0lGgAjAVBdWRpdJSTlCmBlH2UKGgFfZQojBRhZGp1ZGljYXRpb25fc3VtbWFyeZRoFIwLYXNzZXNzbWVudHOUXZSMCmRlYmF0ZV9wcm+UaACMCkRlYmF0ZVNpZGWUk5QpgZR9lChoBX2UKIwEcm9sZZRoFIwIYXJndW1lbnSUaBSMCHJlYnV0dGFslGgUdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoM2g0aDKQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMCmRlYmF0ZV9jb26UaC4pgZR9lChoBX2UKGgyaBRoM2gUaDRoFHVoNX2UaDePlChoM2g0aDKQaDlOdWKMD3BhbmVsX2FncmVlbWVudJSMBXNwbGl0lHVoNX2UaDePlChoKmg6aCloQGgskGg5TnVijApjcmVhdGVkX2F0lIwgMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTYrMDA6MDCUjAttb2RpZmllZF9hdJROjAhsYW5ndWFnZZSMAmVulIwIY292ZXJhZ2WUaACMCENvdmVyYWdllJOUKYGUfZQoaAV9lCiMBnN0YXR1c5SMCXVuY292ZXJlZJSMB3JlYXNvbnOUXZSMBHBsYW6UYYwOY2VydGlmaWNhdGVfaWSUTowPY2VydGlmaWNhdGVfdXJslE6MBWFzX29mlE6MCGN1cnJlbmN5lIwDRVVSlIwDY2FwlE0QJ4wJYWdncmVnYXRllEogoQcAjA10ZXJtc192ZXJzaW9ulIwCdjGUdWg1fZRoN4+UKGhaaFVoWWhWaE9oV2hbaFRoUZBoOU51YnVoNX2UaDePlChoB2gRaBdoRmgcaAtoD2gaaAloDWhEaBVoE2ggaB5oR2hJaCFoI2gZkGg5TnViLg==", + "verify__verification_200_result_json_empty.json": "gASVXgQAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UjA9wcmVzdW1lZF9pbnRlbnSUjACUjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwKbGVuel9zY29yZZRLCYwLa2V5X2ZpbmRpbmeUjC5UaGUgRWFydGggaXMgYXBwcm94aW1hdGVseSBzcGhlcmljYWwgaW4gc2hhcGUulIwRZXhlY3V0aXZlX3N1bW1hcnmUjBZUaGUgY2xhaW0gaXMgdmVyaWZpZWQulIwId2FybmluZ3OUXZSMEXN1Z2dlc3RlZF9yZXdyaXRllE6MB3NvdXJjZXOUXZSMBWF1ZGl0lGgAjAVBdWRpdJSTlCmBlH2UKGgFfZQojBRhZGp1ZGljYXRpb25fc3VtbWFyeZRoFIwLYXNzZXNzbWVudHOUXZSMCmRlYmF0ZV9wcm+UaACMCkRlYmF0ZVNpZGWUk5QpgZR9lChoBX2UKIwEcm9sZZRoFIwIYXJndW1lbnSUaBSMCHJlYnV0dGFslGgUdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoM2g0aDKQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMCmRlYmF0ZV9jb26UaC4pgZR9lChoBX2UKGgyaBRoM2gUaDRoFHVoNX2UaDePlChoM2g0aDKQaDlOdWKMD3BhbmVsX2FncmVlbWVudJSMBXNwbGl0lHVoNX2UaDePlChoKmg6aCloQGgskGg5TnVijApjcmVhdGVkX2F0lIwgMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTYrMDA6MDCUjAttb2RpZmllZF9hdJROjAhsYW5ndWFnZZSMAmVulIwIY292ZXJhZ2WUaACMCENvdmVyYWdllJOUKYGUfZQoaAV9lCiMBnN0YXR1c5SMCXVuY292ZXJlZJSMB3JlYXNvbnOUXZSMBHBsYW6UYYwOY2VydGlmaWNhdGVfaWSUTowPY2VydGlmaWNhdGVfdXJslE6MBWFzX29mlE6MCGN1cnJlbmN5lIwDRVVSlIwDY2FwlE0QJ4wJYWdncmVnYXRllEogoQcAjA10ZXJtc192ZXJzaW9ulIwCdjGUdWg1fZRoN4+UKGhaaFVoWWhWaE9oV2hbaFRoUZBoOU51YnVoNX2UaDePlChoB2gRaBdoRmgcaAtoD2gaaAloDWhEaBVoE2ggaB5oR2hJaCFoI2gZkGg5TnViLg==", + "verify__verification_200_result_json_old_shape_strings.json": "gASVZQUAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UjA9wcmVzdW1lZF9pbnRlbnSUjACUjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwKbGVuel9zY29yZZRLCYwLa2V5X2ZpbmRpbmeUjC5UaGUgRWFydGggaXMgYXBwcm94aW1hdGVseSBzcGhlcmljYWwgaW4gc2hhcGUulIwRZXhlY3V0aXZlX3N1bW1hcnmUjBZUaGUgY2xhaW0gaXMgdmVyaWZpZWQulIwId2FybmluZ3OUXZSMEXN1Z2dlc3RlZF9yZXdyaXRllE6MB3NvdXJjZXOUXZRoAIwGU291cmNllJOUKYGUfZQoaAV9lCiMC3NvdXJjZV9uYW1llGgUjAV0aXRsZZRoFIwDdXJslIwVaHR0cHM6Ly9vbGQuZXhhbXBsZS9hlIwHc25pcHBldJSMCE9sZCByb3culIwEZGF0ZZRoFHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaChoLmgqaCloLJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWF1ZGl0lGgAjAVBdWRpdJSTlCmBlH2UKGgFfZQojBRhZGp1ZGljYXRpb25fc3VtbWFyeZRoFIwLYXNzZXNzbWVudHOUXZRoAIwKQXNzZXNzbWVudJSTlCmBlH2UKGgFfZQojA1wYW5lbGlzdF9uYW1llIwKUmV2aWV3ZXIgQZSMCmZvY3VzX2FyZWGUaBSMBXNjb3JllE6MCXJlYXNvbmluZ5RoFGgeXZR1aC99lGgxj5QoaB5oRGhCaEVoRpBoM051YmGMCmRlYmF0ZV9wcm+UaACMCkRlYmF0ZVNpZGWUk5QpgZR9lChoBX2UKIwEcm9sZZRoFIwIYXJndW1lbnSUaBSMCHJlYnV0dGFslGgUdWgvfZRoMY+UKGhRaFJoUJBoM051YowKZGViYXRlX2NvbpRoTCmBlH2UKGgFfZQoaFBoFGhRaBRoUmgUdWgvfZRoMY+UKGhRaFJoUJBoM051YowPcGFuZWxfYWdyZWVtZW50lIwFc3BsaXSUdWgvfZRoMY+UKGg7aFVoOmhbaEqQaDNOdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKIwGc3RhdHVzlIwJdW5jb3ZlcmVklIwHcmVhc29uc5RdlIwEcGxhbpRhjA5jZXJ0aWZpY2F0ZV9pZJROjA9jZXJ0aWZpY2F0ZV91cmyUTowFYXNfb2aUTowIY3VycmVuY3mUjANFVVKUjANjYXCUTRAnjAlhZ2dyZWdhdGWUSiChBwCMDXRlcm1zX3ZlcnNpb26UjAJ2MZR1aC99lGgxj5QoaHVocGh0aHFoamhyaHZob2hskGgzTnVidWgvfZRoMY+UKGgHaBFoF2hhaBxoC2gPaBpoCWgNaF9oFWgTaCBoHmhiaGRoIWg0aBmQaDNOdWIu", + "verify__verification_by_task_id_200.json": "gASVXAgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UaACMCUVudGl0eVJlZpSTlCmBlH2UKGgFfZQojARuYW1llIwFRWFydGiUjANxaWSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBhoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoG32UaB2PlChoNmg+aDpoOGg8kGgfTnViaDIpgZR9lChoBX2UKGg2jANFU0GUaDiMEEVhcnRoIGZyb20gc3BhY2WUaDqMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaDyMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaD6MCjIwMjUtMDEtMTaUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoK12UdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJoVymBlH2UKGgFfZQoaFuMClJldmlld2VyIEKUaF2MB1NvdXJjZXOUaF9HQCIAAAAAAABoYIwXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaCtdlIwNd2VhayBzb3VyY2UgQZRhdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoG32UaB2PlChod2h5aHWQaB9OdWKMCmRlYmF0ZV9jb26UaHEpgZR9lChoBX2UKGh1jAdTa2VwdGljlGh3jCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGh5jCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBt9lGgdj5QoaHdoeWh1kGgfTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgbfZRoHY+UKGhUaH1oUmiGaG+QaB9OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKIwGc3RhdHVzlIwJdW5jb3ZlcmVklIwHcmVhc29uc5RdlIwEcGxhbpRhjA5jZXJ0aWZpY2F0ZV9pZJROjA9jZXJ0aWZpY2F0ZV91cmyUTowFYXNfb2aUTowIY3VycmVuY3mUjANFVVKUjANjYXCUTRAnjAlhZ2dyZWdhdGWUSiChBwCMDXRlcm1zX3ZlcnNpb26UjAJ2MZR1aBt9lGgdj5QoaKBom2ifaJxolWidaKFommiXkGgfTnVidWgbfZRoHY+UKGgHaBFoJGiMaCloC2gPaCdoCWgNaIpoImggaC5oK2iNaI9oL2hMaCaQaB9OdWIu", + "verify__verification_public_anonymous.json": "gASViQcAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMBnB1YmxpY5SMBWRlcHRolIwIc3RhbmRhcmSUjAZkb21haW6UjAdTY2llbmNllIwIZW50aXRpZXOUXZRoAIwJRW50aXR5UmVmlJOUKYGUfZQoaAV9lCiMBG5hbWWUjAVFYXJ0aJSMA3FpZJROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViYYwPcHJlc3VtZWRfaW50ZW50lIweVmVyaWZ5IGEgYmFzaWMgc2NpZW50aWZpYyBmYWN0lIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMCmxlbnpfc2NvcmWUSwmMC2tleV9maW5kaW5nlIwuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpSMCHdhcm5pbmdzlF2UjBlSZWxpZXMgb24gbGltaXRlZCBzb3VyY2VzlGGMEXN1Z2dlc3RlZF9yZXdyaXRllE6MB3NvdXJjZXOUXZQoaACMBlNvdXJjZZSTlCmBlH2UKGgFfZQojAtzb3VyY2VfbmFtZZSMBE5BU0GUjAV0aXRsZZSMFlRoZSBzaGFwZSBvZiB0aGUgRWFydGiUjAN1cmyUjBpodHRwczovL25hc2EuZXhhbXBsZS9zaGFwZZSMB3NuaXBwZXSUjCtTYXRlbGxpdGUgaW1hZ2VyeSBzaG93cyBhbiBvYmxhdGUgc3BoZXJvaWQulIwEZGF0ZZSMCjIwMjUtMDEtMTWUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJoMimBlH2UKGgFfZQoaDaMA0VTQZRoOIwQRWFydGggZnJvbSBzcGFjZZRoOowZaHR0cHM6Ly9lc2EuZXhhbXBsZS9lYXJ0aJRoPIwrT3JiaXRhbCBtZWFzdXJlbWVudHMgY29uZmlybSB0aGUgY3VydmF0dXJlLpRoPowKMjAyNS0wMS0xNpR1aBt9lGgdj5QoaDZoPmg6aDhoPJBoH051YmWMBWF1ZGl0lGgAjAVBdWRpdJSTlCmBlH2UKGgFfZQojBRhZGp1ZGljYXRpb25fc3VtbWFyeZSMFkJvdGggcmV2aWV3ZXJzIGFncmVlZC6UjAthc3Nlc3NtZW50c5RdlChoAIwKQXNzZXNzbWVudJSTlCmBlH2UKGgFfZQojA1wYW5lbGlzdF9uYW1llIwKUmV2aWV3ZXIgQZSMCmZvY3VzX2FyZWGUjCdDbGFpbSBQcmVjaXNpb24gJiBRdWFudGl0YXRpdmUgQWNjdXJhY3mUjAVzY29yZZRHQCIAAAAAAACMCXJlYXNvbmluZ5SMHVByZWNpc2lvbiBhbmFseXNpcyByZWFzb25pbmculGgrXZR1aBt9lGgdj5QoaCtoXWhbaF9oYJBoH051YmhXKYGUfZQoaAV9lChoW4wKUmV2aWV3ZXIgQpRoXYwHU291cmNlc5RoX0dAIgAAAAAAAGhgjBdTb3VyY2UgYXVkaXQgcmVhc29uaW5nLpRoK12UjA13ZWFrIHNvdXJjZSBBlGF1aBt9lGgdj5QoaCtoXWhbaF9oYJBoH051YmWMCmRlYmF0ZV9wcm+UaACMCkRlYmF0ZVNpZGWUk5QpgZR9lChoBX2UKIwEcm9sZZSMCEFkdm9jYXRllIwIYXJndW1lbnSUjCRTYXRlbGxpdGUgaW1hZ2VyeSBhbmQgZ2VvZGVzeSBhZ3JlZS6UjAhyZWJ1dHRhbJSMIkZsYXQtRWFydGggb2JqZWN0aW9ucyBkbyBub3QgaG9sZC6UdWgbfZRoHY+UKGh3aHlodZBoH051YowKZGViYXRlX2NvbpRocSmBlH2UKGgFfZQoaHWMB1NrZXB0aWOUaHeMIlRoZSBFYXJ0aCBpcyBub3QgYSBwZXJmZWN0IHNwaGVyZS6UaHmMIE9ibGF0ZSwgYnV0IHJvdW5kIGZvciB0aGUgY2xhaW0ulHVoG32UaB2PlChod2h5aHWQaB9OdWKMD3BhbmVsX2FncmVlbWVudJSMCXVuYW5pbW91c5R1aBt9lGgdj5QoaFRofWhSaIZob5BoH051YowKY3JlYXRlZF9hdJSMIDIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2KzAwOjAwlIwLbW9kaWZpZWRfYXSUTowIbGFuZ3VhZ2WUjAJlbpSMCGNvdmVyYWdllE51aBt9lGgdj5QoaAdoEWgkaIxoKWgLaA9oJ2gJaA1oimgiaCBoLmgraI1oj2gvaExoJpBoH051Yi4=", + "verify__verification_public_of_another_user.json": "gASVWwgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMBnB1YmxpY5SMBWRlcHRolIwIc3RhbmRhcmSUjAZkb21haW6UjAdTY2llbmNllIwIZW50aXRpZXOUXZRoAIwJRW50aXR5UmVmlJOUKYGUfZQoaAV9lCiMBG5hbWWUjAVFYXJ0aJSMA3FpZJROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViYYwPcHJlc3VtZWRfaW50ZW50lIweVmVyaWZ5IGEgYmFzaWMgc2NpZW50aWZpYyBmYWN0lIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMCmxlbnpfc2NvcmWUSwmMC2tleV9maW5kaW5nlIwuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpSMCHdhcm5pbmdzlF2UjBlSZWxpZXMgb24gbGltaXRlZCBzb3VyY2VzlGGMEXN1Z2dlc3RlZF9yZXdyaXRllE6MB3NvdXJjZXOUXZQoaACMBlNvdXJjZZSTlCmBlH2UKGgFfZQojAtzb3VyY2VfbmFtZZSMBE5BU0GUjAV0aXRsZZSMFlRoZSBzaGFwZSBvZiB0aGUgRWFydGiUjAN1cmyUjBpodHRwczovL25hc2EuZXhhbXBsZS9zaGFwZZSMB3NuaXBwZXSUjCtTYXRlbGxpdGUgaW1hZ2VyeSBzaG93cyBhbiBvYmxhdGUgc3BoZXJvaWQulIwEZGF0ZZSMCjIwMjUtMDEtMTWUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJoMimBlH2UKGgFfZQoaDaMA0VTQZRoOIwQRWFydGggZnJvbSBzcGFjZZRoOowZaHR0cHM6Ly9lc2EuZXhhbXBsZS9lYXJ0aJRoPIwrT3JiaXRhbCBtZWFzdXJlbWVudHMgY29uZmlybSB0aGUgY3VydmF0dXJlLpRoPowKMjAyNS0wMS0xNpR1aBt9lGgdj5QoaDZoPmg6aDhoPJBoH051YmWMBWF1ZGl0lGgAjAVBdWRpdJSTlCmBlH2UKGgFfZQojBRhZGp1ZGljYXRpb25fc3VtbWFyeZSMFkJvdGggcmV2aWV3ZXJzIGFncmVlZC6UjAthc3Nlc3NtZW50c5RdlChoAIwKQXNzZXNzbWVudJSTlCmBlH2UKGgFfZQojA1wYW5lbGlzdF9uYW1llIwKUmV2aWV3ZXIgQZSMCmZvY3VzX2FyZWGUjCdDbGFpbSBQcmVjaXNpb24gJiBRdWFudGl0YXRpdmUgQWNjdXJhY3mUjAVzY29yZZRHQCIAAAAAAACMCXJlYXNvbmluZ5SMHVByZWNpc2lvbiBhbmFseXNpcyByZWFzb25pbmculGgrXZR1aBt9lGgdj5QoaCtoXWhbaF9oYJBoH051YmhXKYGUfZQoaAV9lChoW4wKUmV2aWV3ZXIgQpRoXYwHU291cmNlc5RoX0dAIgAAAAAAAGhgjBdTb3VyY2UgYXVkaXQgcmVhc29uaW5nLpRoK12UjA13ZWFrIHNvdXJjZSBBlGF1aBt9lGgdj5QoaCtoXWhbaF9oYJBoH051YmWMCmRlYmF0ZV9wcm+UaACMCkRlYmF0ZVNpZGWUk5QpgZR9lChoBX2UKIwEcm9sZZSMCEFkdm9jYXRllIwIYXJndW1lbnSUjCRTYXRlbGxpdGUgaW1hZ2VyeSBhbmQgZ2VvZGVzeSBhZ3JlZS6UjAhyZWJ1dHRhbJSMIkZsYXQtRWFydGggb2JqZWN0aW9ucyBkbyBub3QgaG9sZC6UdWgbfZRoHY+UKGh3aHlodZBoH051YowKZGViYXRlX2NvbpRocSmBlH2UKGgFfZQoaHWMB1NrZXB0aWOUaHeMIlRoZSBFYXJ0aCBpcyBub3QgYSBwZXJmZWN0IHNwaGVyZS6UaHmMIE9ibGF0ZSwgYnV0IHJvdW5kIGZvciB0aGUgY2xhaW0ulHVoG32UaB2PlChod2h5aHWQaB9OdWKMD3BhbmVsX2FncmVlbWVudJSMCXVuYW5pbW91c5R1aBt9lGgdj5QoaFRofWhSaIZob5BoH051YowKY3JlYXRlZF9hdJSMIDIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2KzAwOjAwlIwLbW9kaWZpZWRfYXSUTowIbGFuZ3VhZ2WUjAJlbpSMCGNvdmVyYWdllGgAjAhDb3ZlcmFnZZSTlCmBlH2UKGgFfZQojAZzdGF0dXOUjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoG32UaB2PlChooGibaJ9onGiVaJ1ooWiaaJeQaB9OdWJ1aBt9lGgdj5QoaAdoEWgkaIxoKWgLaA9oJ2gJaA1oimgiaCBoLmgraI1oj2gvaExoJpBoH051Yi4=", + "verify__verification_zero_retention_served.json": "gASVZQgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVmVyaWZpY2F0aW9ulJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMD3ZlcmlmaWNhdGlvbl9pZJSMCDY4Y2E0YWFmlIwFY2xhaW2UjBNUaGUgRWFydGggaXMgcm91bmQulIwKdmlzaWJpbGl0eZSMB3ByaXZhdGWUjAVkZXB0aJSMCHN0YW5kYXJklIwGZG9tYWlulIwHU2NpZW5jZZSMCGVudGl0aWVzlF2UaACMCUVudGl0eVJlZpSTlCmBlH2UKGgFfZQojARuYW1llIwFRWFydGiUjANxaWSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBhoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMJVRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbC6UjBFleGVjdXRpdmVfc3VtbWFyeZSMKFRoZSBjbGFpbSBpcyB3ZWxsIHN1cHBvcnRlZCBieSBldmlkZW5jZS6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoG32UaB2PlChoNmg+aDpoOGg8kGgfTnViaDIpgZR9lChoBX2UKGg2jANFU0GUaDiMEEVhcnRoIGZyb20gc3BhY2WUaDqMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaDyMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaD6MCjIwMjUtMDEtMTaUdWgbfZRoHY+UKGg2aD5oOmg4aDyQaB9OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoK12UdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJoVymBlH2UKGgFfZQoaFuMClJldmlld2VyIEKUaF2MB1NvdXJjZXOUaF9HQCIAAAAAAABoYIwXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaCtdlIwNd2VhayBzb3VyY2UgQZRhdWgbfZRoHY+UKGgraF1oW2hfaGCQaB9OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoG32UaB2PlChod2h5aHWQaB9OdWKMCmRlYmF0ZV9jb26UaHEpgZR9lChoBX2UKGh1jAdTa2VwdGljlGh3jCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGh5jCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBt9lGgdj5QoaHdoeWh1kGgfTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgbfZRoHY+UKGhUaH1oUmiGaG+QaB9OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKIwGc3RhdHVzlIwJdW5jb3ZlcmVklIwHcmVhc29uc5RdlIwEcGxhbpRhjA5jZXJ0aWZpY2F0ZV9pZJROjA9jZXJ0aWZpY2F0ZV91cmyUTowFYXNfb2aUTowIY3VycmVuY3mUjANFVVKUjANjYXCUTRAnjAlhZ2dyZWdhdGWUSiChBwCMDXRlcm1zX3ZlcnNpb26UjAJ2MZR1aBt9lGgdj5QoaKBom2ifaJxolWidaKFommiXkGgfTnVidWgbfZRoHY+UKGgHaBFoJGiMaCloC2gPaCdoCWgNaIpoImggaC5oK2iNaI9oL2hMaCaQaB9OdWIu" +} diff --git a/tests/fixtures/parity/schemas.json b/tests/fixtures/parity/schemas.json new file mode 100644 index 0000000..4c8b1a6 --- /dev/null +++ b/tests/fixtures/parity/schemas.json @@ -0,0 +1,26564 @@ +{ + "AskHistory:serialization": { + "$defs": { + "AskMessage": { + "additionalProperties": true, + "description": "One message in an ``/ask`` conversation thread.", + "properties": { + "content": { + "default": "", + "title": "Content", + "type": "string" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "AskMessage", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Returned by ``GET /ask/{verification_id}``.", + "properties": { + "can_send": { + "default": false, + "title": "Can Send", + "type": "boolean" + }, + "exchange_limit": { + "default": 0, + "title": "Exchange Limit", + "type": "integer" + }, + "exchanges_used": { + "default": 0, + "title": "Exchanges Used", + "type": "integer" + }, + "messages": { + "items": { + "$ref": "#/$defs/AskMessage" + }, + "title": "Messages", + "type": "array" + } + }, + "title": "AskHistory", + "type": "object" + }, + "AskHistory:validation": { + "$defs": { + "AskMessage": { + "additionalProperties": true, + "description": "One message in an ``/ask`` conversation thread.", + "properties": { + "content": { + "default": "", + "title": "Content", + "type": "string" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "AskMessage", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Returned by ``GET /ask/{verification_id}``.", + "properties": { + "can_send": { + "default": false, + "title": "Can Send", + "type": "boolean" + }, + "exchange_limit": { + "default": 0, + "title": "Exchange Limit", + "type": "integer" + }, + "exchanges_used": { + "default": 0, + "title": "Exchanges Used", + "type": "integer" + }, + "messages": { + "items": { + "$ref": "#/$defs/AskMessage" + }, + "title": "Messages", + "type": "array" + } + }, + "title": "AskHistory", + "type": "object" + }, + "AskMessage:serialization": { + "additionalProperties": true, + "description": "One message in an ``/ask`` conversation thread.", + "properties": { + "content": { + "default": "", + "title": "Content", + "type": "string" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "AskMessage", + "type": "object" + }, + "AskMessage:validation": { + "additionalProperties": true, + "description": "One message in an ``/ask`` conversation thread.", + "properties": { + "content": { + "default": "", + "title": "Content", + "type": "string" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "AskMessage", + "type": "object" + }, + "AskReply:serialization": { + "additionalProperties": true, + "description": "Returned by ``POST /ask/{verification_id}``.\n\n``content`` is the assistant's reply text in a small markdown\nsubset:\n\n- ``**bold**`` and ``*italic*``\n- ``- `` or ``* `` bullet lists\n- Blank-line paragraph breaks; single newlines inside a paragraph\n mean line break\n\nThe model only produces these \u2014 no headings, no tables, no code\nblocks. Pass it through any markdown library or display it\nverbatim. See https://lenz.io/docs/quickstart#ask-reply-format.\n\nPre-1.0.2 the SDK declared a single ``reply`` field that never\nmatched the wire \u2014 the server has always returned\n``{role, content, created_at}``. 1.0.2 aligned the typed surface.", + "properties": { + "content": { + "default": "", + "title": "Content", + "type": "string" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "AskReply", + "type": "object" + }, + "AskReply:validation": { + "additionalProperties": true, + "description": "Returned by ``POST /ask/{verification_id}``.\n\n``content`` is the assistant's reply text in a small markdown\nsubset:\n\n- ``**bold**`` and ``*italic*``\n- ``- `` or ``* `` bullet lists\n- Blank-line paragraph breaks; single newlines inside a paragraph\n mean line break\n\nThe model only produces these \u2014 no headings, no tables, no code\nblocks. Pass it through any markdown library or display it\nverbatim. See https://lenz.io/docs/quickstart#ask-reply-format.\n\nPre-1.0.2 the SDK declared a single ``reply`` field that never\nmatched the wire \u2014 the server has always returned\n``{role, content, created_at}``. 1.0.2 aligned the typed surface.", + "properties": { + "content": { + "default": "", + "title": "Content", + "type": "string" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "AskReply", + "type": "object" + }, + "AssessClaim:serialization": { + "additionalProperties": true, + "description": "Per-claim entry in an ``AssessResponse.claims`` list.\n\nLean shape by design \u2014 no model_votes, no panel identity. The\n``verification_url`` (when present) points at the full\n``ClaimDetailOut`` payload at ``GET /api/v1/verifications/{id}`` for\ncallers that want citations and the full audit trail.\n\nA row with ``verdict == \"Error\"`` could not be given a verdict. On a\nlist call it stays in position (one row per item sent), is not charged,\nand says why: ``error_code`` names the cause and ``hint`` is one\nsentence on what to send next. ``hint`` is the field to surface to a\nhuman \u2014 it is written per cause and stays correct as causes are added.\nA vague input is assessed on its most likely reading, which ``claim``\ncarries. A compound input is assessed on its main claim; the other\nclaims found in it are listed in ``identified_claims`` (also with a\n``hint``). These fields default empty so older servers that don't send\nthem still parse.", + "properties": { + "candidate_claims": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidate Claims", + "type": "array" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "AssessClaim", + "type": "object" + }, + "AssessClaim:validation": { + "additionalProperties": true, + "description": "Per-claim entry in an ``AssessResponse.claims`` list.\n\nLean shape by design \u2014 no model_votes, no panel identity. The\n``verification_url`` (when present) points at the full\n``ClaimDetailOut`` payload at ``GET /api/v1/verifications/{id}`` for\ncallers that want citations and the full audit trail.\n\nA row with ``verdict == \"Error\"`` could not be given a verdict. On a\nlist call it stays in position (one row per item sent), is not charged,\nand says why: ``error_code`` names the cause and ``hint`` is one\nsentence on what to send next. ``hint`` is the field to surface to a\nhuman \u2014 it is written per cause and stays correct as causes are added.\nA vague input is assessed on its most likely reading, which ``claim``\ncarries. A compound input is assessed on its main claim; the other\nclaims found in it are listed in ``identified_claims`` (also with a\n``hint``). These fields default empty so older servers that don't send\nthem still parse.", + "properties": { + "candidate_claims": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidate Claims", + "type": "array" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "AssessClaim", + "type": "object" + }, + "AssessResponse:serialization": { + "$defs": { + "AssessClaim": { + "additionalProperties": true, + "description": "Per-claim entry in an ``AssessResponse.claims`` list.\n\nLean shape by design \u2014 no model_votes, no panel identity. The\n``verification_url`` (when present) points at the full\n``ClaimDetailOut`` payload at ``GET /api/v1/verifications/{id}`` for\ncallers that want citations and the full audit trail.\n\nA row with ``verdict == \"Error\"`` could not be given a verdict. On a\nlist call it stays in position (one row per item sent), is not charged,\nand says why: ``error_code`` names the cause and ``hint`` is one\nsentence on what to send next. ``hint`` is the field to surface to a\nhuman \u2014 it is written per cause and stays correct as causes are added.\nA vague input is assessed on its most likely reading, which ``claim``\ncarries. A compound input is assessed on its main claim; the other\nclaims found in it are listed in ``identified_claims`` (also with a\n``hint``). These fields default empty so older servers that don't send\nthem still parse.", + "properties": { + "candidate_claims": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidate Claims", + "type": "array" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "AssessClaim", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Output of ``POST /assess``.\n\nSingle form (``assess(claim=...)``): ``claims`` is one entry per claim\nfound in the input \u2014 up to 20, at 1 credit each. A text that makes more\nclaims than one call checks gets its most check-worthy 20 checked and the\nrest listed in ``more_claims``, unchecked and free: send them back with\n``assess(claims=...)``, 20 a call, to check them. ``error`` is set when\nthe input holds no checkable claim.\n\nList form (``assess(claims=[...])``): exactly one entry per item sent,\nin the order sent. An item that could not be given a verdict is an\n``\"Error\"`` row in position (see ``AssessClaim``), never a missing one;\nthe top-level ``error`` stays ``None``.\n\nWhen ``claims`` is empty (single form), ``error_code`` is ``'no_claim'``:\nthe input holds no checkable claim (a vague input is assessed on its\nmost likely reading instead). It defaults empty, so older servers that\ndon't send it degrade to the plain ``error`` message.", + "properties": { + "candidate_claims": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidate Claims", + "type": "array" + }, + "claims": { + "items": { + "$ref": "#/$defs/AssessClaim" + }, + "title": "Claims", + "type": "array" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error" + }, + "error_code": { + "default": "", + "title": "Error Code", + "type": "string" + }, + "more_claims": { + "items": { + "type": "string" + }, + "title": "More Claims", + "type": "array" + } + }, + "title": "AssessResponse", + "type": "object" + }, + "AssessResponse:validation": { + "$defs": { + "AssessClaim": { + "additionalProperties": true, + "description": "Per-claim entry in an ``AssessResponse.claims`` list.\n\nLean shape by design \u2014 no model_votes, no panel identity. The\n``verification_url`` (when present) points at the full\n``ClaimDetailOut`` payload at ``GET /api/v1/verifications/{id}`` for\ncallers that want citations and the full audit trail.\n\nA row with ``verdict == \"Error\"`` could not be given a verdict. On a\nlist call it stays in position (one row per item sent), is not charged,\nand says why: ``error_code`` names the cause and ``hint`` is one\nsentence on what to send next. ``hint`` is the field to surface to a\nhuman \u2014 it is written per cause and stays correct as causes are added.\nA vague input is assessed on its most likely reading, which ``claim``\ncarries. A compound input is assessed on its main claim; the other\nclaims found in it are listed in ``identified_claims`` (also with a\n``hint``). These fields default empty so older servers that don't send\nthem still parse.", + "properties": { + "candidate_claims": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidate Claims", + "type": "array" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "AssessClaim", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Output of ``POST /assess``.\n\nSingle form (``assess(claim=...)``): ``claims`` is one entry per claim\nfound in the input \u2014 up to 20, at 1 credit each. A text that makes more\nclaims than one call checks gets its most check-worthy 20 checked and the\nrest listed in ``more_claims``, unchecked and free: send them back with\n``assess(claims=...)``, 20 a call, to check them. ``error`` is set when\nthe input holds no checkable claim.\n\nList form (``assess(claims=[...])``): exactly one entry per item sent,\nin the order sent. An item that could not be given a verdict is an\n``\"Error\"`` row in position (see ``AssessClaim``), never a missing one;\nthe top-level ``error`` stays ``None``.\n\nWhen ``claims`` is empty (single form), ``error_code`` is ``'no_claim'``:\nthe input holds no checkable claim (a vague input is assessed on its\nmost likely reading instead). It defaults empty, so older servers that\ndon't send it degrade to the plain ``error`` message.", + "properties": { + "candidate_claims": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidate Claims", + "type": "array" + }, + "claims": { + "items": { + "$ref": "#/$defs/AssessClaim" + }, + "title": "Claims", + "type": "array" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error" + }, + "error_code": { + "default": "", + "title": "Error Code", + "type": "string" + }, + "more_claims": { + "items": { + "type": "string" + }, + "title": "More Claims", + "type": "array" + } + }, + "title": "AssessResponse", + "type": "object" + }, + "Assessment:serialization": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "Assessment:validation": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "Audit:serialization": { + "$defs": { + "Assessment": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "DebateSide": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Nested explainability block \u2014 for callers who want the panel's work.", + "properties": { + "adjudication_summary": { + "default": "", + "title": "Adjudication Summary", + "type": "string" + }, + "assessments": { + "items": { + "$ref": "#/$defs/Assessment" + }, + "title": "Assessments", + "type": "array" + }, + "debate_con": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "debate_pro": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "panel_agreement": { + "default": "", + "title": "Panel Agreement", + "type": "string" + } + }, + "title": "Audit", + "type": "object" + }, + "Audit:validation": { + "$defs": { + "Assessment": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "DebateSide": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Nested explainability block \u2014 for callers who want the panel's work.", + "properties": { + "adjudication_summary": { + "default": "", + "title": "Adjudication Summary", + "type": "string" + }, + "assessments": { + "items": { + "$ref": "#/$defs/Assessment" + }, + "title": "Assessments", + "type": "array" + }, + "debate_con": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "debate_pro": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "panel_agreement": { + "default": "", + "title": "Panel Agreement", + "type": "string" + } + }, + "title": "Audit", + "type": "object" + }, + "BatchAccepted:serialization": { + "$defs": { + "TaskAccepted": { + "additionalProperties": true, + "description": "Returned by ``POST /verify`` and per item of ``POST /verify/batch``.", + "properties": { + "claim_text": { + "default": "", + "title": "Claim Text", + "type": "string" + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + } + }, + "title": "TaskAccepted", + "type": "object" + } + }, + "additionalProperties": true, + "properties": { + "batch_id": { + "default": "", + "title": "Batch Id", + "type": "string" + }, + "items": { + "items": { + "$ref": "#/$defs/TaskAccepted" + }, + "title": "Items", + "type": "array" + } + }, + "title": "BatchAccepted", + "type": "object" + }, + "BatchAccepted:validation": { + "$defs": { + "TaskAccepted": { + "additionalProperties": true, + "description": "Returned by ``POST /verify`` and per item of ``POST /verify/batch``.", + "properties": { + "claim_text": { + "default": "", + "title": "Claim Text", + "type": "string" + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + } + }, + "title": "TaskAccepted", + "type": "object" + } + }, + "additionalProperties": true, + "properties": { + "batch_id": { + "default": "", + "title": "Batch Id", + "type": "string" + }, + "items": { + "items": { + "$ref": "#/$defs/TaskAccepted" + }, + "title": "Items", + "type": "array" + } + }, + "title": "BatchAccepted", + "type": "object" + }, + "BatchItemResult:serialization": { + "$defs": { + "Assessment": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "Audit": { + "additionalProperties": true, + "description": "Nested explainability block \u2014 for callers who want the panel's work.", + "properties": { + "adjudication_summary": { + "default": "", + "title": "Adjudication Summary", + "type": "string" + }, + "assessments": { + "items": { + "$ref": "#/$defs/Assessment" + }, + "title": "Assessments", + "type": "array" + }, + "debate_con": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "debate_pro": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "panel_agreement": { + "default": "", + "title": "Panel Agreement", + "type": "string" + } + }, + "title": "Audit", + "type": "object" + }, + "CandidateClaim": { + "additionalProperties": true, + "description": "One of multiple distinct claims framing found in the submitted text.", + "properties": { + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "CandidateClaim", + "type": "object" + }, + "Coverage": { + "additionalProperties": true, + "description": "Whether this verdict carries Lenz's warranty, for YOUR account.\n\nPresent only when covered verification is enabled on the account's plan;\n``None`` otherwise. It describes the pair (your account, this analysis),\nnot the analysis alone \u2014 two accounts can hold two certificates over one\ncached verdict, so the block you see is never another customer's.\n\n``reasons`` is empty exactly when ``status`` is not ``\"uncovered\"``.\n\nThe money fields are three, not two: ``currency`` is ISO 4217 and the\namounts are integers in **major units** \u2014 ``cap=10000`` means ten\nthousand, not a hundred. They are contract figures, not amounts a payment\nprocessor charges. Read ``currency``; do not assume EUR.", + "properties": { + "aggregate": { + "default": 0, + "title": "Aggregate", + "type": "integer" + }, + "as_of": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "As Of" + }, + "cap": { + "default": 0, + "title": "Cap", + "type": "integer" + }, + "certificate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Id" + }, + "certificate_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Url" + }, + "currency": { + "default": "", + "title": "Currency", + "type": "string" + }, + "reasons": { + "items": { + "type": "string" + }, + "title": "Reasons", + "type": "array" + }, + "status": { + "default": "uncovered", + "title": "Status", + "type": "string" + }, + "terms_version": { + "default": "", + "title": "Terms Version", + "type": "string" + } + }, + "title": "Coverage", + "type": "object" + }, + "DebateSide": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + }, + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "Progress": { + "additionalProperties": true, + "description": "Where a running verification has got to. Advisory \u2014 never results.\n\n``step`` is one of ``starting`` / ``framing`` / ``research`` / ``debate``\n/ ``adjudication`` / ``conclusion``. ``index`` is the 1-based stage\nposition (0 while ``starting``) out of ``total``; read ``total`` off the\nresponse rather than hard-coding it. ``poll_after_seconds`` is how long\nto wait before looking again \u2014 :meth:`Lenz.verify_and_wait` honours it\nfor you. ``elapsed_seconds`` is how long the run has been going, a\nmeasurement rather than an estimate of what is left.\n\n``index`` is stage POSITION, not elapsed work: the stages are uneven, so\na bar driven by it sits on ``research`` for roughly half the run.\n\n``step`` stays a plain ``str``, not a ``Literal`` \u2014 an SDK that\nhard-rejects a stage name the server adds later is worse than one that\npasses it through.\n\nThis was a plain ``dict`` up to 2.10.0. The mapping methods below keep\n``p[\"step\"]``, ``\"step\" in p``, ``p.keys()`` and ``dict(p)`` working on a\n2.x minor; they are deprecated and go in 3.0.0. Use attributes.", + "properties": { + "elapsed_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Elapsed Seconds" + }, + "index": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Index" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "step": { + "default": "", + "title": "Step", + "type": "string" + }, + "total": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Total" + } + }, + "title": "Progress", + "type": "object" + }, + "SimilarVerification": { + "additionalProperties": true, + "description": "An existing public verification that semantically resembles the submitted text.\n\nSame vocabulary as ``Verification`` \u2014 flat ``verdict`` / ``confidence`` /\n``lenz_score`` at top level, no nested ``Verdict`` object.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "distance": { + "default": 0.0, + "title": "Distance", + "type": "number" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "SimilarVerification", + "type": "object" + }, + "Source": { + "additionalProperties": true, + "description": "A single citation backing a verification.", + "properties": { + "date": { + "default": "", + "title": "Date", + "type": "string" + }, + "snippet": { + "default": "", + "title": "Snippet", + "type": "string" + }, + "source_name": { + "default": "", + "title": "Source Name", + "type": "string" + }, + "title": { + "default": "", + "title": "Title", + "type": "string" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + } + }, + "title": "Source", + "type": "object" + }, + "TaskStatus": { + "additionalProperties": true, + "description": "Returned by ``GET /verify/status/{task_id}``.", + "properties": { + "candidates": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidates", + "type": "array" + }, + "claims": { + "items": { + "$ref": "#/$defs/CandidateClaim" + }, + "title": "Claims", + "type": "array" + }, + "docs_url": { + "default": "", + "title": "Docs Url", + "type": "string" + }, + "error": { + "default": "", + "title": "Error", + "type": "string" + }, + "failure_class": { + "default": "", + "title": "Failure Class", + "type": "string" + }, + "failure_detail": { + "default": "", + "title": "Failure Detail", + "type": "string" + }, + "failure_reason": { + "default": "", + "title": "Failure Reason", + "type": "string" + }, + "hint": { + "default": "", + "title": "Hint", + "type": "string" + }, + "progress": { + "$ref": "#/$defs/Progress" + }, + "reason": { + "default": "", + "title": "Reason", + "type": "string" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/Verification" + }, + { + "type": "null" + } + ], + "default": null + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "similar_claims": { + "deprecated": true, + "items": { + "$ref": "#/$defs/SimilarVerification" + }, + "title": "Similar Claims", + "type": "array" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + } + }, + "title": "TaskStatus", + "type": "object" + }, + "Verification": { + "additionalProperties": true, + "description": "Full verification report \u2014 returned by ``verify_and_wait``,\n``verifications.get``, the ``/verify/status/{task_id}`` polling\nendpoint, and the webhook payload.\n\nThe verdict block is FLAT at top level (was nested ``Verdict`` object\npre-unify). ``created_at`` + ``modified_at`` are the only timestamp\nfields on the API surface \u2014 editorial ``published_at`` is internal-only.\n\n1.1.0: dropped ``url`` and ``visibility``. API claims are private by\ndefault and referenced by ``verification_id`` only. Cache-hit on\nanother customer's claim is transparent \u2014 the customer always sees\ntheir own ``verification_id``, never another customer's.\n\nLater: ``visibility`` returns \u2014 'private' | 'unlisted' | 'public'. It\nechoes what you set on submit ('private'/'unlisted' are settable;\n'public' can only be read, for listed claims). ``url`` stays dropped.", + "properties": { + "audit": { + "$ref": "#/$defs/Audit" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "coverage": { + "anyOf": [ + { + "$ref": "#/$defs/Coverage" + }, + { + "type": "null" + } + ], + "default": null + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "default": "", + "title": "Depth", + "type": "string" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "presumed_intent": { + "default": "", + "title": "Presumed Intent", + "type": "string" + }, + "sources": { + "items": { + "$ref": "#/$defs/Source" + }, + "title": "Sources", + "type": "array" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + }, + "visibility": { + "default": "", + "title": "Visibility", + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Verification", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Per-item outcome from :meth:`Lenz.verify_batch_and_wait`.\n\nA client-side composition type \u2014 NOT a wire shape (the server never emits\nit, so it has no contract fixture). One entry per task that\n``POST /verify/batch`` returned, in input order.\n\n``status`` is a client-side rollup:\n\n- ``completed`` \u2014 ``verification`` is set (and ``status_detail`` carries the raw poll).\n- ``needs_input`` \u2014 paused for caller input; inspect ``status_detail`` (reason / claims).\n- ``failed`` \u2014 terminal failure (or completed-without-result); ``status_detail`` carries the diagnostic.\n A verification removed by its account's retention period (HTTP 410) is ``failed``, ``status_detail`` ``None``.\n- ``timeout`` \u2014 the deadline elapsed before this task reached a terminal state; ``status_detail`` is ``None``.", + "properties": { + "claim_text": { + "default": "", + "title": "Claim Text", + "type": "string" + }, + "status": { + "enum": [ + "completed", + "needs_input", + "failed", + "timeout" + ], + "title": "Status", + "type": "string" + }, + "status_detail": { + "anyOf": [ + { + "$ref": "#/$defs/TaskStatus" + }, + { + "type": "null" + } + ], + "default": null + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + }, + "verification": { + "anyOf": [ + { + "$ref": "#/$defs/Verification" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "status" + ], + "title": "BatchItemResult", + "type": "object" + }, + "BatchItemResult:validation": { + "$defs": { + "Assessment": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "Audit": { + "additionalProperties": true, + "description": "Nested explainability block \u2014 for callers who want the panel's work.", + "properties": { + "adjudication_summary": { + "default": "", + "title": "Adjudication Summary", + "type": "string" + }, + "assessments": { + "items": { + "$ref": "#/$defs/Assessment" + }, + "title": "Assessments", + "type": "array" + }, + "debate_con": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "debate_pro": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "panel_agreement": { + "default": "", + "title": "Panel Agreement", + "type": "string" + } + }, + "title": "Audit", + "type": "object" + }, + "CandidateClaim": { + "additionalProperties": true, + "description": "One of multiple distinct claims framing found in the submitted text.", + "properties": { + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "CandidateClaim", + "type": "object" + }, + "Coverage": { + "additionalProperties": true, + "description": "Whether this verdict carries Lenz's warranty, for YOUR account.\n\nPresent only when covered verification is enabled on the account's plan;\n``None`` otherwise. It describes the pair (your account, this analysis),\nnot the analysis alone \u2014 two accounts can hold two certificates over one\ncached verdict, so the block you see is never another customer's.\n\n``reasons`` is empty exactly when ``status`` is not ``\"uncovered\"``.\n\nThe money fields are three, not two: ``currency`` is ISO 4217 and the\namounts are integers in **major units** \u2014 ``cap=10000`` means ten\nthousand, not a hundred. They are contract figures, not amounts a payment\nprocessor charges. Read ``currency``; do not assume EUR.", + "properties": { + "aggregate": { + "default": 0, + "title": "Aggregate", + "type": "integer" + }, + "as_of": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "As Of" + }, + "cap": { + "default": 0, + "title": "Cap", + "type": "integer" + }, + "certificate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Id" + }, + "certificate_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Url" + }, + "currency": { + "default": "", + "title": "Currency", + "type": "string" + }, + "reasons": { + "items": { + "type": "string" + }, + "title": "Reasons", + "type": "array" + }, + "status": { + "default": "uncovered", + "title": "Status", + "type": "string" + }, + "terms_version": { + "default": "", + "title": "Terms Version", + "type": "string" + } + }, + "title": "Coverage", + "type": "object" + }, + "DebateSide": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + }, + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "Progress": { + "additionalProperties": true, + "description": "Where a running verification has got to. Advisory \u2014 never results.\n\n``step`` is one of ``starting`` / ``framing`` / ``research`` / ``debate``\n/ ``adjudication`` / ``conclusion``. ``index`` is the 1-based stage\nposition (0 while ``starting``) out of ``total``; read ``total`` off the\nresponse rather than hard-coding it. ``poll_after_seconds`` is how long\nto wait before looking again \u2014 :meth:`Lenz.verify_and_wait` honours it\nfor you. ``elapsed_seconds`` is how long the run has been going, a\nmeasurement rather than an estimate of what is left.\n\n``index`` is stage POSITION, not elapsed work: the stages are uneven, so\na bar driven by it sits on ``research`` for roughly half the run.\n\n``step`` stays a plain ``str``, not a ``Literal`` \u2014 an SDK that\nhard-rejects a stage name the server adds later is worse than one that\npasses it through.\n\nThis was a plain ``dict`` up to 2.10.0. The mapping methods below keep\n``p[\"step\"]``, ``\"step\" in p``, ``p.keys()`` and ``dict(p)`` working on a\n2.x minor; they are deprecated and go in 3.0.0. Use attributes.", + "properties": { + "elapsed_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Elapsed Seconds" + }, + "index": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Index" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "step": { + "default": "", + "title": "Step", + "type": "string" + }, + "total": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Total" + } + }, + "title": "Progress", + "type": "object" + }, + "SimilarVerification": { + "additionalProperties": true, + "description": "An existing public verification that semantically resembles the submitted text.\n\nSame vocabulary as ``Verification`` \u2014 flat ``verdict`` / ``confidence`` /\n``lenz_score`` at top level, no nested ``Verdict`` object.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "distance": { + "default": 0.0, + "title": "Distance", + "type": "number" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "SimilarVerification", + "type": "object" + }, + "Source": { + "additionalProperties": true, + "description": "A single citation backing a verification.", + "properties": { + "date": { + "default": "", + "title": "Date", + "type": "string" + }, + "snippet": { + "default": "", + "title": "Snippet", + "type": "string" + }, + "source_name": { + "default": "", + "title": "Source Name", + "type": "string" + }, + "title": { + "default": "", + "title": "Title", + "type": "string" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + } + }, + "title": "Source", + "type": "object" + }, + "TaskStatus": { + "additionalProperties": true, + "description": "Returned by ``GET /verify/status/{task_id}``.", + "properties": { + "candidates": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidates", + "type": "array" + }, + "claims": { + "items": { + "$ref": "#/$defs/CandidateClaim" + }, + "title": "Claims", + "type": "array" + }, + "docs_url": { + "default": "", + "title": "Docs Url", + "type": "string" + }, + "error": { + "default": "", + "title": "Error", + "type": "string" + }, + "failure_class": { + "default": "", + "title": "Failure Class", + "type": "string" + }, + "failure_detail": { + "default": "", + "title": "Failure Detail", + "type": "string" + }, + "failure_reason": { + "default": "", + "title": "Failure Reason", + "type": "string" + }, + "hint": { + "default": "", + "title": "Hint", + "type": "string" + }, + "progress": { + "$ref": "#/$defs/Progress" + }, + "reason": { + "default": "", + "title": "Reason", + "type": "string" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/Verification" + }, + { + "type": "null" + } + ], + "default": null + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "similar_claims": { + "deprecated": true, + "items": { + "$ref": "#/$defs/SimilarVerification" + }, + "title": "Similar Claims", + "type": "array" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + } + }, + "title": "TaskStatus", + "type": "object" + }, + "Verification": { + "additionalProperties": true, + "description": "Full verification report \u2014 returned by ``verify_and_wait``,\n``verifications.get``, the ``/verify/status/{task_id}`` polling\nendpoint, and the webhook payload.\n\nThe verdict block is FLAT at top level (was nested ``Verdict`` object\npre-unify). ``created_at`` + ``modified_at`` are the only timestamp\nfields on the API surface \u2014 editorial ``published_at`` is internal-only.\n\n1.1.0: dropped ``url`` and ``visibility``. API claims are private by\ndefault and referenced by ``verification_id`` only. Cache-hit on\nanother customer's claim is transparent \u2014 the customer always sees\ntheir own ``verification_id``, never another customer's.\n\nLater: ``visibility`` returns \u2014 'private' | 'unlisted' | 'public'. It\nechoes what you set on submit ('private'/'unlisted' are settable;\n'public' can only be read, for listed claims). ``url`` stays dropped.", + "properties": { + "audit": { + "$ref": "#/$defs/Audit" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "coverage": { + "anyOf": [ + { + "$ref": "#/$defs/Coverage" + }, + { + "type": "null" + } + ], + "default": null + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "default": "", + "title": "Depth", + "type": "string" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "presumed_intent": { + "default": "", + "title": "Presumed Intent", + "type": "string" + }, + "sources": { + "items": { + "$ref": "#/$defs/Source" + }, + "title": "Sources", + "type": "array" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + }, + "visibility": { + "default": "", + "title": "Visibility", + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Verification", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Per-item outcome from :meth:`Lenz.verify_batch_and_wait`.\n\nA client-side composition type \u2014 NOT a wire shape (the server never emits\nit, so it has no contract fixture). One entry per task that\n``POST /verify/batch`` returned, in input order.\n\n``status`` is a client-side rollup:\n\n- ``completed`` \u2014 ``verification`` is set (and ``status_detail`` carries the raw poll).\n- ``needs_input`` \u2014 paused for caller input; inspect ``status_detail`` (reason / claims).\n- ``failed`` \u2014 terminal failure (or completed-without-result); ``status_detail`` carries the diagnostic.\n A verification removed by its account's retention period (HTTP 410) is ``failed``, ``status_detail`` ``None``.\n- ``timeout`` \u2014 the deadline elapsed before this task reached a terminal state; ``status_detail`` is ``None``.", + "properties": { + "claim_text": { + "default": "", + "title": "Claim Text", + "type": "string" + }, + "status": { + "enum": [ + "completed", + "needs_input", + "failed", + "timeout" + ], + "title": "Status", + "type": "string" + }, + "status_detail": { + "anyOf": [ + { + "$ref": "#/$defs/TaskStatus" + }, + { + "type": "null" + } + ], + "default": null + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + }, + "verification": { + "anyOf": [ + { + "$ref": "#/$defs/Verification" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "status" + ], + "title": "BatchItemResult", + "type": "object" + }, + "CandidateClaim:serialization": { + "additionalProperties": true, + "description": "One of multiple distinct claims framing found in the submitted text.", + "properties": { + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "CandidateClaim", + "type": "object" + }, + "CandidateClaim:validation": { + "additionalProperties": true, + "description": "One of multiple distinct claims framing found in the submitted text.", + "properties": { + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "CandidateClaim", + "type": "object" + }, + "Citecheck:serialization": { + "$defs": { + "CitecheckPolicy": { + "additionalProperties": true, + "description": "``max_citations``: how many citations are checked (for pairs, the\nnumber of pairs: every pair is checked).", + "properties": { + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + } + }, + "title": "CitecheckPolicy", + "type": "object" + }, + "CitecheckSummary": { + "additionalProperties": true, + "description": "Counts over the check, as on a review's ``summary``. ``citations_found``\nis the citations in the text, or the pairs sent; ``None`` until read.", + "properties": { + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + } + }, + "title": "CitecheckSummary", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewCitation": { + "additionalProperties": true, + "description": "One citation of the draft (a URL or a DOI where the draft uses it), in\nthe draft's order. Only on a review that asked for the citation check.\n\n``reference`` is the citation as the draft writes it; ``statement`` the\ndraft's sentence it is attached to, with link syntax reduced to its\nwords; ``quotes`` the words the draft quotes from this source.\n``position`` locates ``statement`` in the text as sent (its ``text`` is\n``None``: the row carries the ``statement``). ``result`` is\n``None`` until the check has ended, and on a failed row with nothing\nestablished.", + "properties": { + "check": { + "$ref": "#/$defs/ReviewCitationCheck" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitation", + "type": "object" + }, + "ReviewCitationCheck": { + "additionalProperties": true, + "description": "A citation's check. ``status`` is progress, as on ``assessment``:\n``pending`` | ``running`` | ``completed`` | ``failed``.\n\n- ``page_read``: ``full``, ``partial``, ``not_found`` or ``none``.\n- ``source_url``: where the text was actually read from (after\n redirects, or a free copy of a paper). ``source_version``, for a DOI:\n ``published``, ``accepted`` or ``submitted``.\n- ``support``: ``supported``, ``partly_supported``, ``contradicted``,\n ``not_in_source`` or ``unchecked``. ``snippet`` is the verified passage\n of the source it rests on; ``rationale`` is a reviewer's note, not a\n checked source.\n- ``quote``: ``matched``, ``not_in_source`` or ``unchecked``; ``None``\n when the draft quoted nothing from this source. ``missing_quote`` is\n the quoted excerpt the quote check did not find; ``None`` unless\n ``quote`` is ``not_in_source``.\n- ``doi_registered``, ``metadata`` (``consistent``, ``mismatch`` or\n ``unchecked``), ``metadata_differences`` and ``registered``: set for a\n DOI only.\n- ``unchecked_reason`` and ``hint``: why the finding is ``unchecked`` and\n what to do next. The reasons are an open set, e.g. ``no_text``,\n ``partial_text``, ``login_required``, ``unsupported_site``,\n ``no_statement``, ``invalid_url``, ``other_version``, ``inconclusive``,\n ``ambiguous_reference``.\n- ``failure``: on a ``failed`` check.", + "properties": { + "doi_registered": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi Registered" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "metadata": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Metadata" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Language" + }, + "page_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Published Date" + }, + "page_read": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Read" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quote" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "registered": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationRecord" + }, + { + "type": "null" + } + ], + "default": null + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Version" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "support": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Support" + }, + "unchecked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unchecked Reason" + } + }, + "title": "ReviewCitationCheck", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationFailure": { + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationIssue": { + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCitationRecord": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + }, + "ReviewCitationResult": { + "additionalProperties": true, + "description": "The one answer to read for a citation, derived from its ``check``.\n\n``finding``, most serious first: ``doi_not_found``, ``page_not_found``,\n``contradicted``, ``quote_not_in_source``, ``not_in_source``,\n``partly_supported``, ``metadata_mismatch`` (each an issue), then\n``supported`` and ``unchecked``. ``source`` says which check the finding\ncame from: ``doi``, ``page``, ``support``, ``quote`` or ``metadata``;\n``None`` on ``unchecked``.", + "properties": { + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + } + }, + "title": "ReviewCitationResult", + "type": "object" + }, + "ReviewCredits": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewMoreCitation": { + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + } + }, + "additionalProperties": true, + "description": "``GET /citechecks/{citecheck_id}``: a citation check as it stands.\n\n``status``: ``queued`` \u2192 ``checking`` \u2192 ``completed``, or ``failed``.\n``outcome`` is ``None`` until it ends, then ``clean``, ``issues_found``,\n``incomplete`` (a citation could not be checked for a reason of ours) or\n``unchecked``. ``citations``, ``citation_issues`` and ``citation_failures``\nare the review's rows. ``more_citations`` lists the citations of the text\npast the ones checked; ``None`` until the text is read, ``[]`` for pairs.", + "properties": { + "citation_failures": { + "items": { + "$ref": "#/$defs/ReviewCitationFailure" + }, + "title": "Citation Failures", + "type": "array" + }, + "citation_issues": { + "items": { + "$ref": "#/$defs/ReviewCitationIssue" + }, + "title": "Citation Issues", + "type": "array" + }, + "citations": { + "items": { + "$ref": "#/$defs/ReviewCitation" + }, + "title": "Citations", + "type": "array" + }, + "citecheck_id": { + "default": "", + "title": "Citecheck Id", + "type": "string" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed At" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "credits": { + "$ref": "#/$defs/ReviewCredits" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ReviewMoreCitation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Citations" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Outcome" + }, + "policy": { + "$ref": "#/$defs/CitecheckPolicy" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/CitecheckSummary" + } + }, + "title": "Citecheck", + "type": "object" + }, + "Citecheck:validation": { + "$defs": { + "CitecheckPolicy": { + "additionalProperties": true, + "description": "``max_citations``: how many citations are checked (for pairs, the\nnumber of pairs: every pair is checked).", + "properties": { + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + } + }, + "title": "CitecheckPolicy", + "type": "object" + }, + "CitecheckSummary": { + "additionalProperties": true, + "description": "Counts over the check, as on a review's ``summary``. ``citations_found``\nis the citations in the text, or the pairs sent; ``None`` until read.", + "properties": { + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + } + }, + "title": "CitecheckSummary", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewCitation": { + "additionalProperties": true, + "description": "One citation of the draft (a URL or a DOI where the draft uses it), in\nthe draft's order. Only on a review that asked for the citation check.\n\n``reference`` is the citation as the draft writes it; ``statement`` the\ndraft's sentence it is attached to, with link syntax reduced to its\nwords; ``quotes`` the words the draft quotes from this source.\n``position`` locates ``statement`` in the text as sent (its ``text`` is\n``None``: the row carries the ``statement``). ``result`` is\n``None`` until the check has ended, and on a failed row with nothing\nestablished.", + "properties": { + "check": { + "$ref": "#/$defs/ReviewCitationCheck" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitation", + "type": "object" + }, + "ReviewCitationCheck": { + "additionalProperties": true, + "description": "A citation's check. ``status`` is progress, as on ``assessment``:\n``pending`` | ``running`` | ``completed`` | ``failed``.\n\n- ``page_read``: ``full``, ``partial``, ``not_found`` or ``none``.\n- ``source_url``: where the text was actually read from (after\n redirects, or a free copy of a paper). ``source_version``, for a DOI:\n ``published``, ``accepted`` or ``submitted``.\n- ``support``: ``supported``, ``partly_supported``, ``contradicted``,\n ``not_in_source`` or ``unchecked``. ``snippet`` is the verified passage\n of the source it rests on; ``rationale`` is a reviewer's note, not a\n checked source.\n- ``quote``: ``matched``, ``not_in_source`` or ``unchecked``; ``None``\n when the draft quoted nothing from this source. ``missing_quote`` is\n the quoted excerpt the quote check did not find; ``None`` unless\n ``quote`` is ``not_in_source``.\n- ``doi_registered``, ``metadata`` (``consistent``, ``mismatch`` or\n ``unchecked``), ``metadata_differences`` and ``registered``: set for a\n DOI only.\n- ``unchecked_reason`` and ``hint``: why the finding is ``unchecked`` and\n what to do next. The reasons are an open set, e.g. ``no_text``,\n ``partial_text``, ``login_required``, ``unsupported_site``,\n ``no_statement``, ``invalid_url``, ``other_version``, ``inconclusive``,\n ``ambiguous_reference``.\n- ``failure``: on a ``failed`` check.", + "properties": { + "doi_registered": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi Registered" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "metadata": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Metadata" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Language" + }, + "page_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Published Date" + }, + "page_read": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Read" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quote" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "registered": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationRecord" + }, + { + "type": "null" + } + ], + "default": null + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Version" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "support": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Support" + }, + "unchecked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unchecked Reason" + } + }, + "title": "ReviewCitationCheck", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationFailure": { + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationIssue": { + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCitationRecord": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + }, + "ReviewCitationResult": { + "additionalProperties": true, + "description": "The one answer to read for a citation, derived from its ``check``.\n\n``finding``, most serious first: ``doi_not_found``, ``page_not_found``,\n``contradicted``, ``quote_not_in_source``, ``not_in_source``,\n``partly_supported``, ``metadata_mismatch`` (each an issue), then\n``supported`` and ``unchecked``. ``source`` says which check the finding\ncame from: ``doi``, ``page``, ``support``, ``quote`` or ``metadata``;\n``None`` on ``unchecked``.", + "properties": { + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + } + }, + "title": "ReviewCitationResult", + "type": "object" + }, + "ReviewCredits": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewMoreCitation": { + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + } + }, + "additionalProperties": true, + "description": "``GET /citechecks/{citecheck_id}``: a citation check as it stands.\n\n``status``: ``queued`` \u2192 ``checking`` \u2192 ``completed``, or ``failed``.\n``outcome`` is ``None`` until it ends, then ``clean``, ``issues_found``,\n``incomplete`` (a citation could not be checked for a reason of ours) or\n``unchecked``. ``citations``, ``citation_issues`` and ``citation_failures``\nare the review's rows. ``more_citations`` lists the citations of the text\npast the ones checked; ``None`` until the text is read, ``[]`` for pairs.", + "properties": { + "citation_failures": { + "items": { + "$ref": "#/$defs/ReviewCitationFailure" + }, + "title": "Citation Failures", + "type": "array" + }, + "citation_issues": { + "items": { + "$ref": "#/$defs/ReviewCitationIssue" + }, + "title": "Citation Issues", + "type": "array" + }, + "citations": { + "items": { + "$ref": "#/$defs/ReviewCitation" + }, + "title": "Citations", + "type": "array" + }, + "citecheck_id": { + "default": "", + "title": "Citecheck Id", + "type": "string" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed At" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "credits": { + "$ref": "#/$defs/ReviewCredits" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ReviewMoreCitation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Citations" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Outcome" + }, + "policy": { + "$ref": "#/$defs/CitecheckPolicy" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/CitecheckSummary" + } + }, + "title": "Citecheck", + "type": "object" + }, + "CitecheckPolicy:serialization": { + "additionalProperties": true, + "description": "``max_citations``: how many citations are checked (for pairs, the\nnumber of pairs: every pair is checked).", + "properties": { + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + } + }, + "title": "CitecheckPolicy", + "type": "object" + }, + "CitecheckPolicy:validation": { + "additionalProperties": true, + "description": "``max_citations``: how many citations are checked (for pairs, the\nnumber of pairs: every pair is checked).", + "properties": { + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + } + }, + "title": "CitecheckPolicy", + "type": "object" + }, + "CitecheckStarted:serialization": { + "additionalProperties": true, + "description": "Returned by ``POST /citecheck`` (HTTP 202). ``status`` is always\n``\"queued\"``: the receipt, not the current state.", + "properties": { + "citecheck_id": { + "default": "", + "title": "Citecheck Id", + "type": "string" + }, + "status": { + "default": "queued", + "title": "Status", + "type": "string" + } + }, + "title": "CitecheckStarted", + "type": "object" + }, + "CitecheckStarted:validation": { + "additionalProperties": true, + "description": "Returned by ``POST /citecheck`` (HTTP 202). ``status`` is always\n``\"queued\"``: the receipt, not the current state.", + "properties": { + "citecheck_id": { + "default": "", + "title": "Citecheck Id", + "type": "string" + }, + "status": { + "default": "queued", + "title": "Status", + "type": "string" + } + }, + "title": "CitecheckStarted", + "type": "object" + }, + "CitecheckSummary:serialization": { + "$defs": { + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Counts over the check, as on a review's ``summary``. ``citations_found``\nis the citations in the text, or the pairs sent; ``None`` until read.", + "properties": { + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + } + }, + "title": "CitecheckSummary", + "type": "object" + }, + "CitecheckSummary:validation": { + "$defs": { + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Counts over the check, as on a review's ``summary``. ``citations_found``\nis the citations in the text, or the pairs sent; ``None`` until read.", + "properties": { + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + } + }, + "title": "CitecheckSummary", + "type": "object" + }, + "ClaimLocation:serialization": { + "$defs": { + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "ClaimLocation:validation": { + "$defs": { + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "DebateSide:serialization": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + }, + "DebateSide:validation": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + }, + "EntityRef:serialization": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "EntityRef:validation": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "Escalation:serialization": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "Escalation:validation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "EscalationPolicy:serialization": { + "additionalProperties": true, + "description": "Which quick-checked claims get a deep check, as the review RESOLVED it.\n\nA claim is deep-checked when its quick verdict is in ``verdicts`` OR its\nconfidence band is in ``confidence``, up to ``max_verifications`` of them,\nat ``depth``. ``max_assessments`` is how many of the draft's claims, most\ncheck-worthy first, got a quick verdict.", + "properties": { + "confidence": { + "items": { + "type": "string" + }, + "title": "Confidence", + "type": "array" + }, + "depth": { + "default": "standard", + "title": "Depth", + "type": "string" + }, + "max_assessments": { + "default": 20, + "title": "Max Assessments", + "type": "integer" + }, + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + }, + "max_verifications": { + "default": 5, + "title": "Max Verifications", + "type": "integer" + }, + "suggest_edits": { + "default": false, + "title": "Suggest Edits", + "type": "boolean" + }, + "verdicts": { + "items": { + "type": "string" + }, + "title": "Verdicts", + "type": "array" + } + }, + "title": "EscalationPolicy", + "type": "object" + }, + "EscalationPolicy:validation": { + "additionalProperties": true, + "description": "Which quick-checked claims get a deep check, as the review RESOLVED it.\n\nA claim is deep-checked when its quick verdict is in ``verdicts`` OR its\nconfidence band is in ``confidence``, up to ``max_verifications`` of them,\nat ``depth``. ``max_assessments`` is how many of the draft's claims, most\ncheck-worthy first, got a quick verdict.", + "properties": { + "confidence": { + "items": { + "type": "string" + }, + "title": "Confidence", + "type": "array" + }, + "depth": { + "default": "standard", + "title": "Depth", + "type": "string" + }, + "max_assessments": { + "default": 20, + "title": "Max Assessments", + "type": "integer" + }, + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + }, + "max_verifications": { + "default": 5, + "title": "Max Verifications", + "type": "integer" + }, + "suggest_edits": { + "default": false, + "title": "Suggest Edits", + "type": "boolean" + }, + "verdicts": { + "items": { + "type": "string" + }, + "title": "Verdicts", + "type": "array" + } + }, + "title": "EscalationPolicy", + "type": "object" + }, + "ExtractedClaims:serialization": { + "$defs": { + "ClaimLocation": { + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "ExtractedEntity": { + "additionalProperties": true, + "description": "An entity surfaced by ``/extract``. ``type`` is the framing category\n(``person`` | ``org`` | ``place`` | ``topic``).", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "type": { + "default": "", + "title": "Type", + "type": "string" + } + }, + "title": "ExtractedEntity", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Output of ``POST /extract``.\n\n``status`` is one of ``ExtractStatus``: ``ready``, ``not_a_claim``, or \u2014\nwhen a ``focus`` was given and no claim fell within it \u2014 ``no_match``.\n``no_match`` is a successful answer, not an error: ``identified_claims``\nis empty and the unfocused list is never substituted for it.\n\n``locations`` is set only on a call made with ``locate=True``: one\n``ClaimLocation`` per returned claim, in the order of\n``identified_claims`` (one entry for a single ``claim``). It is ``[]``\nwhen every claim was left out (``status`` is then ``\"not_a_claim\"``),\nand ``None`` when ``locate`` was not set, when the extraction found no\nclaims, or when the\nclaims could not be located (the list is then returned unfiltered).\nOlder servers omit the key; it parses as ``None``.", + "properties": { + "candidate_claims": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidate Claims", + "type": "array" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "key_entities": { + "items": { + "$ref": "#/$defs/ExtractedEntity" + }, + "title": "Key Entities", + "type": "array" + }, + "locations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ClaimLocation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Locations" + }, + "original_input": { + "default": "", + "title": "Original Input", + "type": "string" + }, + "presumed_intent": { + "default": "", + "title": "Presumed Intent", + "type": "string" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "ExtractedClaims", + "type": "object" + }, + "ExtractedClaims:validation": { + "$defs": { + "ClaimLocation": { + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "ExtractedEntity": { + "additionalProperties": true, + "description": "An entity surfaced by ``/extract``. ``type`` is the framing category\n(``person`` | ``org`` | ``place`` | ``topic``).", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "type": { + "default": "", + "title": "Type", + "type": "string" + } + }, + "title": "ExtractedEntity", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Output of ``POST /extract``.\n\n``status`` is one of ``ExtractStatus``: ``ready``, ``not_a_claim``, or \u2014\nwhen a ``focus`` was given and no claim fell within it \u2014 ``no_match``.\n``no_match`` is a successful answer, not an error: ``identified_claims``\nis empty and the unfocused list is never substituted for it.\n\n``locations`` is set only on a call made with ``locate=True``: one\n``ClaimLocation`` per returned claim, in the order of\n``identified_claims`` (one entry for a single ``claim``). It is ``[]``\nwhen every claim was left out (``status`` is then ``\"not_a_claim\"``),\nand ``None`` when ``locate`` was not set, when the extraction found no\nclaims, or when the\nclaims could not be located (the list is then returned unfiltered).\nOlder servers omit the key; it parses as ``None``.", + "properties": { + "candidate_claims": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidate Claims", + "type": "array" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "key_entities": { + "items": { + "$ref": "#/$defs/ExtractedEntity" + }, + "title": "Key Entities", + "type": "array" + }, + "locations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ClaimLocation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Locations" + }, + "original_input": { + "default": "", + "title": "Original Input", + "type": "string" + }, + "presumed_intent": { + "default": "", + "title": "Presumed Intent", + "type": "string" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "ExtractedClaims", + "type": "object" + }, + "ExtractedEntity:serialization": { + "additionalProperties": true, + "description": "An entity surfaced by ``/extract``. ``type`` is the framing category\n(``person`` | ``org`` | ``place`` | ``topic``).", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "type": { + "default": "", + "title": "Type", + "type": "string" + } + }, + "title": "ExtractedEntity", + "type": "object" + }, + "ExtractedEntity:validation": { + "additionalProperties": true, + "description": "An entity surfaced by ``/extract``. ``type`` is the framing category\n(``person`` | ``org`` | ``place`` | ``topic``).", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "type": { + "default": "", + "title": "Type", + "type": "string" + } + }, + "title": "ExtractedEntity", + "type": "object" + }, + "FailureBlock:serialization": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "FailureBlock:validation": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "LibraryItem:serialization": { + "$defs": { + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Same shape as VerificationListItem on the public Library list.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "LibraryItem", + "type": "object" + }, + "LibraryItem:validation": { + "$defs": { + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Same shape as VerificationListItem on the public Library list.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "LibraryItem", + "type": "object" + }, + "LibraryList:serialization": { + "$defs": { + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "LibraryItem": { + "additionalProperties": true, + "description": "Same shape as VerificationListItem on the public Library list.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "LibraryItem", + "type": "object" + } + }, + "additionalProperties": true, + "properties": { + "items": { + "items": { + "$ref": "#/$defs/LibraryItem" + }, + "title": "Items", + "type": "array" + }, + "page": { + "default": 1, + "title": "Page", + "type": "integer" + }, + "page_size": { + "default": 20, + "title": "Page Size", + "type": "integer" + }, + "total": { + "default": 0, + "title": "Total", + "type": "integer" + } + }, + "title": "LibraryList", + "type": "object" + }, + "LibraryList:validation": { + "$defs": { + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "LibraryItem": { + "additionalProperties": true, + "description": "Same shape as VerificationListItem on the public Library list.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "LibraryItem", + "type": "object" + } + }, + "additionalProperties": true, + "properties": { + "items": { + "items": { + "$ref": "#/$defs/LibraryItem" + }, + "title": "Items", + "type": "array" + }, + "page": { + "default": 1, + "title": "Page", + "type": "integer" + }, + "page_size": { + "default": 20, + "title": "Page Size", + "type": "integer" + }, + "total": { + "default": 0, + "title": "Total", + "type": "integer" + } + }, + "title": "LibraryList", + "type": "object" + }, + "Position:serialization": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "Position:validation": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "RelatedVerifications:serialization": { + "$defs": { + "SimilarVerification": { + "additionalProperties": true, + "description": "An existing public verification that semantically resembles the submitted text.\n\nSame vocabulary as ``Verification`` \u2014 flat ``verdict`` / ``confidence`` /\n``lenz_score`` at top level, no nested ``Verdict`` object.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "distance": { + "default": 0.0, + "title": "Distance", + "type": "number" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "SimilarVerification", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Wrapper for ``GET /verifications/{id}/related``.", + "properties": { + "items": { + "items": { + "$ref": "#/$defs/SimilarVerification" + }, + "title": "Items", + "type": "array" + } + }, + "title": "RelatedVerifications", + "type": "object" + }, + "RelatedVerifications:validation": { + "$defs": { + "SimilarVerification": { + "additionalProperties": true, + "description": "An existing public verification that semantically resembles the submitted text.\n\nSame vocabulary as ``Verification`` \u2014 flat ``verdict`` / ``confidence`` /\n``lenz_score`` at top level, no nested ``Verdict`` object.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "distance": { + "default": 0.0, + "title": "Distance", + "type": "number" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "SimilarVerification", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Wrapper for ``GET /verifications/{id}/related``.", + "properties": { + "items": { + "items": { + "$ref": "#/$defs/SimilarVerification" + }, + "title": "Items", + "type": "array" + } + }, + "title": "RelatedVerifications", + "type": "object" + }, + "ReviewAssessment:serialization": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A claim's quick check. ``status``: ``pending`` | ``running`` |\n``completed`` | ``failed``. On ``completed`` the fields mean what they\nmean on an ``/assess`` row; on ``failed`` see ``error_code`` (an open\nset, as on ``/assess``), ``hint`` and ``failure``.", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "ReviewAssessment", + "type": "object" + }, + "ReviewAssessment:validation": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A claim's quick check. ``status``: ``pending`` | ``running`` |\n``completed`` | ``failed``. On ``completed`` the fields mean what they\nmean on an ``/assess`` row; on ``failed`` see ``error_code`` (an open\nset, as on ``/assess``), ``hint`` and ``failure``.", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "ReviewAssessment", + "type": "object" + }, + "ReviewAssessmentCounts:serialization": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewAssessmentCounts:validation": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewCitation:serialization": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewCitationCheck": { + "additionalProperties": true, + "description": "A citation's check. ``status`` is progress, as on ``assessment``:\n``pending`` | ``running`` | ``completed`` | ``failed``.\n\n- ``page_read``: ``full``, ``partial``, ``not_found`` or ``none``.\n- ``source_url``: where the text was actually read from (after\n redirects, or a free copy of a paper). ``source_version``, for a DOI:\n ``published``, ``accepted`` or ``submitted``.\n- ``support``: ``supported``, ``partly_supported``, ``contradicted``,\n ``not_in_source`` or ``unchecked``. ``snippet`` is the verified passage\n of the source it rests on; ``rationale`` is a reviewer's note, not a\n checked source.\n- ``quote``: ``matched``, ``not_in_source`` or ``unchecked``; ``None``\n when the draft quoted nothing from this source. ``missing_quote`` is\n the quoted excerpt the quote check did not find; ``None`` unless\n ``quote`` is ``not_in_source``.\n- ``doi_registered``, ``metadata`` (``consistent``, ``mismatch`` or\n ``unchecked``), ``metadata_differences`` and ``registered``: set for a\n DOI only.\n- ``unchecked_reason`` and ``hint``: why the finding is ``unchecked`` and\n what to do next. The reasons are an open set, e.g. ``no_text``,\n ``partial_text``, ``login_required``, ``unsupported_site``,\n ``no_statement``, ``invalid_url``, ``other_version``, ``inconclusive``,\n ``ambiguous_reference``.\n- ``failure``: on a ``failed`` check.", + "properties": { + "doi_registered": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi Registered" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "metadata": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Metadata" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Language" + }, + "page_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Published Date" + }, + "page_read": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Read" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quote" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "registered": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationRecord" + }, + { + "type": "null" + } + ], + "default": null + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Version" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "support": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Support" + }, + "unchecked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unchecked Reason" + } + }, + "title": "ReviewCitationCheck", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationRecord": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + }, + "ReviewCitationResult": { + "additionalProperties": true, + "description": "The one answer to read for a citation, derived from its ``check``.\n\n``finding``, most serious first: ``doi_not_found``, ``page_not_found``,\n``contradicted``, ``quote_not_in_source``, ``not_in_source``,\n``partly_supported``, ``metadata_mismatch`` (each an issue), then\n``supported`` and ``unchecked``. ``source`` says which check the finding\ncame from: ``doi``, ``page``, ``support``, ``quote`` or ``metadata``;\n``None`` on ``unchecked``.", + "properties": { + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + } + }, + "title": "ReviewCitationResult", + "type": "object" + } + }, + "additionalProperties": true, + "description": "One citation of the draft (a URL or a DOI where the draft uses it), in\nthe draft's order. Only on a review that asked for the citation check.\n\n``reference`` is the citation as the draft writes it; ``statement`` the\ndraft's sentence it is attached to, with link syntax reduced to its\nwords; ``quotes`` the words the draft quotes from this source.\n``position`` locates ``statement`` in the text as sent (its ``text`` is\n``None``: the row carries the ``statement``). ``result`` is\n``None`` until the check has ended, and on a failed row with nothing\nestablished.", + "properties": { + "check": { + "$ref": "#/$defs/ReviewCitationCheck" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitation", + "type": "object" + }, + "ReviewCitation:validation": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewCitationCheck": { + "additionalProperties": true, + "description": "A citation's check. ``status`` is progress, as on ``assessment``:\n``pending`` | ``running`` | ``completed`` | ``failed``.\n\n- ``page_read``: ``full``, ``partial``, ``not_found`` or ``none``.\n- ``source_url``: where the text was actually read from (after\n redirects, or a free copy of a paper). ``source_version``, for a DOI:\n ``published``, ``accepted`` or ``submitted``.\n- ``support``: ``supported``, ``partly_supported``, ``contradicted``,\n ``not_in_source`` or ``unchecked``. ``snippet`` is the verified passage\n of the source it rests on; ``rationale`` is a reviewer's note, not a\n checked source.\n- ``quote``: ``matched``, ``not_in_source`` or ``unchecked``; ``None``\n when the draft quoted nothing from this source. ``missing_quote`` is\n the quoted excerpt the quote check did not find; ``None`` unless\n ``quote`` is ``not_in_source``.\n- ``doi_registered``, ``metadata`` (``consistent``, ``mismatch`` or\n ``unchecked``), ``metadata_differences`` and ``registered``: set for a\n DOI only.\n- ``unchecked_reason`` and ``hint``: why the finding is ``unchecked`` and\n what to do next. The reasons are an open set, e.g. ``no_text``,\n ``partial_text``, ``login_required``, ``unsupported_site``,\n ``no_statement``, ``invalid_url``, ``other_version``, ``inconclusive``,\n ``ambiguous_reference``.\n- ``failure``: on a ``failed`` check.", + "properties": { + "doi_registered": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi Registered" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "metadata": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Metadata" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Language" + }, + "page_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Published Date" + }, + "page_read": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Read" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quote" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "registered": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationRecord" + }, + { + "type": "null" + } + ], + "default": null + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Version" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "support": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Support" + }, + "unchecked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unchecked Reason" + } + }, + "title": "ReviewCitationCheck", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationRecord": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + }, + "ReviewCitationResult": { + "additionalProperties": true, + "description": "The one answer to read for a citation, derived from its ``check``.\n\n``finding``, most serious first: ``doi_not_found``, ``page_not_found``,\n``contradicted``, ``quote_not_in_source``, ``not_in_source``,\n``partly_supported``, ``metadata_mismatch`` (each an issue), then\n``supported`` and ``unchecked``. ``source`` says which check the finding\ncame from: ``doi``, ``page``, ``support``, ``quote`` or ``metadata``;\n``None`` on ``unchecked``.", + "properties": { + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + } + }, + "title": "ReviewCitationResult", + "type": "object" + } + }, + "additionalProperties": true, + "description": "One citation of the draft (a URL or a DOI where the draft uses it), in\nthe draft's order. Only on a review that asked for the citation check.\n\n``reference`` is the citation as the draft writes it; ``statement`` the\ndraft's sentence it is attached to, with link syntax reduced to its\nwords; ``quotes`` the words the draft quotes from this source.\n``position`` locates ``statement`` in the text as sent (its ``text`` is\n``None``: the row carries the ``statement``). ``result`` is\n``None`` until the check has ended, and on a failed row with nothing\nestablished.", + "properties": { + "check": { + "$ref": "#/$defs/ReviewCitationCheck" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitation", + "type": "object" + }, + "ReviewCitationCheck:serialization": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationRecord": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A citation's check. ``status`` is progress, as on ``assessment``:\n``pending`` | ``running`` | ``completed`` | ``failed``.\n\n- ``page_read``: ``full``, ``partial``, ``not_found`` or ``none``.\n- ``source_url``: where the text was actually read from (after\n redirects, or a free copy of a paper). ``source_version``, for a DOI:\n ``published``, ``accepted`` or ``submitted``.\n- ``support``: ``supported``, ``partly_supported``, ``contradicted``,\n ``not_in_source`` or ``unchecked``. ``snippet`` is the verified passage\n of the source it rests on; ``rationale`` is a reviewer's note, not a\n checked source.\n- ``quote``: ``matched``, ``not_in_source`` or ``unchecked``; ``None``\n when the draft quoted nothing from this source. ``missing_quote`` is\n the quoted excerpt the quote check did not find; ``None`` unless\n ``quote`` is ``not_in_source``.\n- ``doi_registered``, ``metadata`` (``consistent``, ``mismatch`` or\n ``unchecked``), ``metadata_differences`` and ``registered``: set for a\n DOI only.\n- ``unchecked_reason`` and ``hint``: why the finding is ``unchecked`` and\n what to do next. The reasons are an open set, e.g. ``no_text``,\n ``partial_text``, ``login_required``, ``unsupported_site``,\n ``no_statement``, ``invalid_url``, ``other_version``, ``inconclusive``,\n ``ambiguous_reference``.\n- ``failure``: on a ``failed`` check.", + "properties": { + "doi_registered": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi Registered" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "metadata": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Metadata" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Language" + }, + "page_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Published Date" + }, + "page_read": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Read" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quote" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "registered": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationRecord" + }, + { + "type": "null" + } + ], + "default": null + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Version" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "support": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Support" + }, + "unchecked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unchecked Reason" + } + }, + "title": "ReviewCitationCheck", + "type": "object" + }, + "ReviewCitationCheck:validation": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationRecord": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A citation's check. ``status`` is progress, as on ``assessment``:\n``pending`` | ``running`` | ``completed`` | ``failed``.\n\n- ``page_read``: ``full``, ``partial``, ``not_found`` or ``none``.\n- ``source_url``: where the text was actually read from (after\n redirects, or a free copy of a paper). ``source_version``, for a DOI:\n ``published``, ``accepted`` or ``submitted``.\n- ``support``: ``supported``, ``partly_supported``, ``contradicted``,\n ``not_in_source`` or ``unchecked``. ``snippet`` is the verified passage\n of the source it rests on; ``rationale`` is a reviewer's note, not a\n checked source.\n- ``quote``: ``matched``, ``not_in_source`` or ``unchecked``; ``None``\n when the draft quoted nothing from this source. ``missing_quote`` is\n the quoted excerpt the quote check did not find; ``None`` unless\n ``quote`` is ``not_in_source``.\n- ``doi_registered``, ``metadata`` (``consistent``, ``mismatch`` or\n ``unchecked``), ``metadata_differences`` and ``registered``: set for a\n DOI only.\n- ``unchecked_reason`` and ``hint``: why the finding is ``unchecked`` and\n what to do next. The reasons are an open set, e.g. ``no_text``,\n ``partial_text``, ``login_required``, ``unsupported_site``,\n ``no_statement``, ``invalid_url``, ``other_version``, ``inconclusive``,\n ``ambiguous_reference``.\n- ``failure``: on a ``failed`` check.", + "properties": { + "doi_registered": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi Registered" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "metadata": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Metadata" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Language" + }, + "page_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Published Date" + }, + "page_read": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Read" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quote" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "registered": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationRecord" + }, + { + "type": "null" + } + ], + "default": null + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Version" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "support": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Support" + }, + "unchecked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unchecked Reason" + } + }, + "title": "ReviewCitationCheck", + "type": "object" + }, + "ReviewCitationCheckCounts:serialization": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationCheckCounts:validation": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationDifference:serialization": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationDifference:validation": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationFailure:serialization": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationFailure:validation": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationIssue:serialization": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCitationIssue:validation": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCitationRecord:serialization": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + }, + "ReviewCitationRecord:validation": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + }, + "ReviewCitationResult:serialization": { + "additionalProperties": true, + "description": "The one answer to read for a citation, derived from its ``check``.\n\n``finding``, most serious first: ``doi_not_found``, ``page_not_found``,\n``contradicted``, ``quote_not_in_source``, ``not_in_source``,\n``partly_supported``, ``metadata_mismatch`` (each an issue), then\n``supported`` and ``unchecked``. ``source`` says which check the finding\ncame from: ``doi``, ``page``, ``support``, ``quote`` or ``metadata``;\n``None`` on ``unchecked``.", + "properties": { + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + } + }, + "title": "ReviewCitationResult", + "type": "object" + }, + "ReviewCitationResult:validation": { + "additionalProperties": true, + "description": "The one answer to read for a citation, derived from its ``check``.\n\n``finding``, most serious first: ``doi_not_found``, ``page_not_found``,\n``contradicted``, ``quote_not_in_source``, ``not_in_source``,\n``partly_supported``, ``metadata_mismatch`` (each an issue), then\n``supported`` and ``unchecked``. ``source`` says which check the finding\ncame from: ``doi``, ``page``, ``support``, ``quote`` or ``metadata``;\n``None`` on ``unchecked``.", + "properties": { + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + } + }, + "title": "ReviewCitationResult", + "type": "object" + }, + "ReviewClaim:serialization": { + "$defs": { + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewAssessment": { + "additionalProperties": true, + "description": "A claim's quick check. ``status``: ``pending`` | ``running`` |\n``completed`` | ``failed``. On ``completed`` the fields mean what they\nmean on an ``/assess`` row; on ``failed`` see ``error_code`` (an open\nset, as on ``/assess``), ``hint`` and ``failure``.", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "ReviewAssessment", + "type": "object" + }, + "ReviewEntity": { + "additionalProperties": true, + "description": "An entity the deep check found in the claim. ``qid`` is its Wikidata\nid when resolved. Either may be ``None``.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "ReviewEntity", + "type": "object" + }, + "ReviewResult": { + "additionalProperties": true, + "description": "The one answer to read for a claim: the deep check's verdict when it\ncompleted, else the quick check's. ``source`` says which\n(``assessment`` or ``verification``).", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + } + }, + "title": "ReviewResult", + "type": "object" + }, + "ReviewVerification": { + "additionalProperties": true, + "description": "A claim's deep check. ``status``: ``processing`` | ``completed`` |\n``failed``; while ``processing`` only ``status``, ``content_status`` and\nthe ids are set. ``content_status`` is ``purged`` once the verification\nwas deleted or removed by its account's retention period: the verdict and\nids stay, the text fields and links are ``None``.\n\n``visibility`` and ``depth`` are what the verification actually has: a\nverdict served from an earlier check keeps its own visibility and may be\n``standard`` for a ``low`` request. Sources are on the verification\nitself: ``client.verifications.get(verification_id)``, or ``url``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "content_status": { + "default": "available", + "title": "Content Status", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Depth" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Domain" + }, + "entities": { + "items": { + "$ref": "#/$defs/ReviewEntity" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Executive Summary" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "task_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Task Id" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "visibility": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Visibility" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "ReviewVerification", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "One claim of the draft, in the order the draft's claims were read.\n\n``result`` is ``None`` until the quick check completes, and on a failed\none. ``escalation`` is ``None`` until then too. ``verification`` is set\nonce a deep check was planned for the claim.\n\n``positions`` lists every place the draft makes the claim, in text\norder, at most 10 (see ``Position``). ``start`` / ``end`` index the\n``text`` you sent in Unicode code points (``text[start:end]`` in Python;\n``end`` exclusive), the same coordinates as a citation's ``position``.\nFor a URL draft, ``start`` and ``end`` are ``None`` and ``text`` carries\nthe passage. ``positions`` is ``None`` when the claims could not be\nlocated, or once a zero-retention draft is gone (and from servers that\npredate the field).", + "properties": { + "assessment": { + "$ref": "#/$defs/ReviewAssessment" + }, + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "verification": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerification" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewClaim", + "type": "object" + }, + "ReviewClaim:validation": { + "$defs": { + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewAssessment": { + "additionalProperties": true, + "description": "A claim's quick check. ``status``: ``pending`` | ``running`` |\n``completed`` | ``failed``. On ``completed`` the fields mean what they\nmean on an ``/assess`` row; on ``failed`` see ``error_code`` (an open\nset, as on ``/assess``), ``hint`` and ``failure``.", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "ReviewAssessment", + "type": "object" + }, + "ReviewEntity": { + "additionalProperties": true, + "description": "An entity the deep check found in the claim. ``qid`` is its Wikidata\nid when resolved. Either may be ``None``.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "ReviewEntity", + "type": "object" + }, + "ReviewResult": { + "additionalProperties": true, + "description": "The one answer to read for a claim: the deep check's verdict when it\ncompleted, else the quick check's. ``source`` says which\n(``assessment`` or ``verification``).", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + } + }, + "title": "ReviewResult", + "type": "object" + }, + "ReviewVerification": { + "additionalProperties": true, + "description": "A claim's deep check. ``status``: ``processing`` | ``completed`` |\n``failed``; while ``processing`` only ``status``, ``content_status`` and\nthe ids are set. ``content_status`` is ``purged`` once the verification\nwas deleted or removed by its account's retention period: the verdict and\nids stay, the text fields and links are ``None``.\n\n``visibility`` and ``depth`` are what the verification actually has: a\nverdict served from an earlier check keeps its own visibility and may be\n``standard`` for a ``low`` request. Sources are on the verification\nitself: ``client.verifications.get(verification_id)``, or ``url``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "content_status": { + "default": "available", + "title": "Content Status", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Depth" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Domain" + }, + "entities": { + "items": { + "$ref": "#/$defs/ReviewEntity" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Executive Summary" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "task_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Task Id" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "visibility": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Visibility" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "ReviewVerification", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "One claim of the draft, in the order the draft's claims were read.\n\n``result`` is ``None`` until the quick check completes, and on a failed\none. ``escalation`` is ``None`` until then too. ``verification`` is set\nonce a deep check was planned for the claim.\n\n``positions`` lists every place the draft makes the claim, in text\norder, at most 10 (see ``Position``). ``start`` / ``end`` index the\n``text`` you sent in Unicode code points (``text[start:end]`` in Python;\n``end`` exclusive), the same coordinates as a citation's ``position``.\nFor a URL draft, ``start`` and ``end`` are ``None`` and ``text`` carries\nthe passage. ``positions`` is ``None`` when the claims could not be\nlocated, or once a zero-retention draft is gone (and from servers that\npredate the field).", + "properties": { + "assessment": { + "$ref": "#/$defs/ReviewAssessment" + }, + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "verification": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerification" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewClaim", + "type": "object" + }, + "ReviewCredits:serialization": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewCredits:validation": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewEntity:serialization": { + "additionalProperties": true, + "description": "An entity the deep check found in the claim. ``qid`` is its Wikidata\nid when resolved. Either may be ``None``.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "ReviewEntity", + "type": "object" + }, + "ReviewEntity:validation": { + "additionalProperties": true, + "description": "An entity the deep check found in the claim. ``qid`` is its Wikidata\nid when resolved. Either may be ``None``.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "ReviewEntity", + "type": "object" + }, + "ReviewEnvelope:serialization": { + "$defs": { + "ClaimLocation": { + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "EscalationPolicy": { + "additionalProperties": true, + "description": "Which quick-checked claims get a deep check, as the review RESOLVED it.\n\nA claim is deep-checked when its quick verdict is in ``verdicts`` OR its\nconfidence band is in ``confidence``, up to ``max_verifications`` of them,\nat ``depth``. ``max_assessments`` is how many of the draft's claims, most\ncheck-worthy first, got a quick verdict.", + "properties": { + "confidence": { + "items": { + "type": "string" + }, + "title": "Confidence", + "type": "array" + }, + "depth": { + "default": "standard", + "title": "Depth", + "type": "string" + }, + "max_assessments": { + "default": 20, + "title": "Max Assessments", + "type": "integer" + }, + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + }, + "max_verifications": { + "default": 5, + "title": "Max Verifications", + "type": "integer" + }, + "suggest_edits": { + "default": false, + "title": "Suggest Edits", + "type": "boolean" + }, + "verdicts": { + "items": { + "type": "string" + }, + "title": "Verdicts", + "type": "array" + } + }, + "title": "EscalationPolicy", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewAssessmentCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationFailure": { + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationIssue": { + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCredits": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewFailure": { + "additionalProperties": true, + "description": "A claim outside the issues whose work failed. ``stage`` is\n``assessment`` or ``verification``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "stage": { + "default": "", + "title": "Stage", + "type": "string" + } + }, + "title": "ReviewFailure", + "type": "object" + }, + "ReviewIssue": { + "additionalProperties": true, + "description": "A claim whose final verdict is ``False``, ``Mostly False`` or ``Mixed``.\n\n``source`` says which check the verdict comes from: ``verification`` (a\ndeep check, with ``verification_id``, ``url`` and ``key_finding``) or\n``assessment`` (the quick check only; ``escalation`` says why it was not\ndeep-checked, and ``rationale`` is the reviewers' note).\n``verified_claim`` is the deep check's wording of the claim when it\ndiffers from ``claim``. ``failure`` is set when the claim's deep check\nfailed.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Status" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "verified_claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verified Claim" + } + }, + "title": "ReviewIssue", + "type": "object" + }, + "ReviewMoreCitation": { + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + }, + "ReviewSummary": { + "additionalProperties": true, + "description": "Counts over the review. A count is ``None`` until what it counts is\nknown (``claims_selected`` before the draft has been read).\n\nThe ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review\nthat did not ask for the citation check.", + "properties": { + "assessments": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewAssessmentCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + }, + "citations_skipped": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Skipped" + }, + "claim_limit": { + "default": 20, + "title": "Claim Limit", + "type": "integer" + }, + "claim_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim Limit Reached" + }, + "claims_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claims Selected" + }, + "input_truncated": { + "default": false, + "title": "Input Truncated", + "type": "boolean" + }, + "issues": { + "default": 0, + "title": "Issues", + "type": "integer" + }, + "verifications": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerificationCounts" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewSummary", + "type": "object" + }, + "ReviewVerificationCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "What both views of ``GET /reviews/{review_id}`` carry.\n\n``status`` is the lifecycle: ``queued`` \u2192 ``assessing`` \u2192 ``verifying``\n\u2192 ``completed``, or ``failed``. ``outcome`` is ``None`` until the review\nends, then one of ``clean`` (every selected claim checked, no issue),\n``issues_found``, ``incomplete`` (some work failed) or ``unchecked`` (it\nfailed before any claim was checked; ``failure`` says why).\n\nOn a review that asked for the citation check, a citation issue makes\n``outcome`` ``issues_found`` too (``issues`` may then be empty: the rows\nare in ``citation_issues``), and a failed citation check makes it\n``incomplete``. ``status`` reads ``verifying`` while deep checks or\ncitation checks are running.\n\n``issues`` and ``failures`` can change until ``completed``.\n``poll_after_seconds`` is how long to wait before reading again; ``None``\nonce the review has ended.", + "properties": { + "citation_failures": { + "items": { + "$ref": "#/$defs/ReviewCitationFailure" + }, + "title": "Citation Failures", + "type": "array" + }, + "citation_issues": { + "items": { + "$ref": "#/$defs/ReviewCitationIssue" + }, + "title": "Citation Issues", + "type": "array" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed At" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "credits": { + "$ref": "#/$defs/ReviewCredits" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "failures": { + "items": { + "$ref": "#/$defs/ReviewFailure" + }, + "title": "Failures", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/$defs/ReviewIssue" + }, + "title": "Issues", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ReviewMoreCitation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Citations" + }, + "more_claim_locations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ClaimLocation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claim Locations" + }, + "more_claims": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claims" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Outcome" + }, + "policy": { + "$ref": "#/$defs/EscalationPolicy" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "review_id": { + "default": "", + "title": "Review Id", + "type": "string" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/ReviewSummary" + }, + "view": { + "default": "", + "title": "View", + "type": "string" + } + }, + "title": "ReviewEnvelope", + "type": "object" + }, + "ReviewEnvelope:validation": { + "$defs": { + "ClaimLocation": { + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "EscalationPolicy": { + "additionalProperties": true, + "description": "Which quick-checked claims get a deep check, as the review RESOLVED it.\n\nA claim is deep-checked when its quick verdict is in ``verdicts`` OR its\nconfidence band is in ``confidence``, up to ``max_verifications`` of them,\nat ``depth``. ``max_assessments`` is how many of the draft's claims, most\ncheck-worthy first, got a quick verdict.", + "properties": { + "confidence": { + "items": { + "type": "string" + }, + "title": "Confidence", + "type": "array" + }, + "depth": { + "default": "standard", + "title": "Depth", + "type": "string" + }, + "max_assessments": { + "default": 20, + "title": "Max Assessments", + "type": "integer" + }, + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + }, + "max_verifications": { + "default": 5, + "title": "Max Verifications", + "type": "integer" + }, + "suggest_edits": { + "default": false, + "title": "Suggest Edits", + "type": "boolean" + }, + "verdicts": { + "items": { + "type": "string" + }, + "title": "Verdicts", + "type": "array" + } + }, + "title": "EscalationPolicy", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewAssessmentCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationFailure": { + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationIssue": { + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCredits": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewFailure": { + "additionalProperties": true, + "description": "A claim outside the issues whose work failed. ``stage`` is\n``assessment`` or ``verification``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "stage": { + "default": "", + "title": "Stage", + "type": "string" + } + }, + "title": "ReviewFailure", + "type": "object" + }, + "ReviewIssue": { + "additionalProperties": true, + "description": "A claim whose final verdict is ``False``, ``Mostly False`` or ``Mixed``.\n\n``source`` says which check the verdict comes from: ``verification`` (a\ndeep check, with ``verification_id``, ``url`` and ``key_finding``) or\n``assessment`` (the quick check only; ``escalation`` says why it was not\ndeep-checked, and ``rationale`` is the reviewers' note).\n``verified_claim`` is the deep check's wording of the claim when it\ndiffers from ``claim``. ``failure`` is set when the claim's deep check\nfailed.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Status" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "verified_claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verified Claim" + } + }, + "title": "ReviewIssue", + "type": "object" + }, + "ReviewMoreCitation": { + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + }, + "ReviewSummary": { + "additionalProperties": true, + "description": "Counts over the review. A count is ``None`` until what it counts is\nknown (``claims_selected`` before the draft has been read).\n\nThe ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review\nthat did not ask for the citation check.", + "properties": { + "assessments": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewAssessmentCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + }, + "citations_skipped": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Skipped" + }, + "claim_limit": { + "default": 20, + "title": "Claim Limit", + "type": "integer" + }, + "claim_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim Limit Reached" + }, + "claims_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claims Selected" + }, + "input_truncated": { + "default": false, + "title": "Input Truncated", + "type": "boolean" + }, + "issues": { + "default": 0, + "title": "Issues", + "type": "integer" + }, + "verifications": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerificationCounts" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewSummary", + "type": "object" + }, + "ReviewVerificationCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "What both views of ``GET /reviews/{review_id}`` carry.\n\n``status`` is the lifecycle: ``queued`` \u2192 ``assessing`` \u2192 ``verifying``\n\u2192 ``completed``, or ``failed``. ``outcome`` is ``None`` until the review\nends, then one of ``clean`` (every selected claim checked, no issue),\n``issues_found``, ``incomplete`` (some work failed) or ``unchecked`` (it\nfailed before any claim was checked; ``failure`` says why).\n\nOn a review that asked for the citation check, a citation issue makes\n``outcome`` ``issues_found`` too (``issues`` may then be empty: the rows\nare in ``citation_issues``), and a failed citation check makes it\n``incomplete``. ``status`` reads ``verifying`` while deep checks or\ncitation checks are running.\n\n``issues`` and ``failures`` can change until ``completed``.\n``poll_after_seconds`` is how long to wait before reading again; ``None``\nonce the review has ended.", + "properties": { + "citation_failures": { + "items": { + "$ref": "#/$defs/ReviewCitationFailure" + }, + "title": "Citation Failures", + "type": "array" + }, + "citation_issues": { + "items": { + "$ref": "#/$defs/ReviewCitationIssue" + }, + "title": "Citation Issues", + "type": "array" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed At" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "credits": { + "$ref": "#/$defs/ReviewCredits" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "failures": { + "items": { + "$ref": "#/$defs/ReviewFailure" + }, + "title": "Failures", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/$defs/ReviewIssue" + }, + "title": "Issues", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ReviewMoreCitation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Citations" + }, + "more_claim_locations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ClaimLocation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claim Locations" + }, + "more_claims": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claims" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Outcome" + }, + "policy": { + "$ref": "#/$defs/EscalationPolicy" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "review_id": { + "default": "", + "title": "Review Id", + "type": "string" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/ReviewSummary" + }, + "view": { + "default": "", + "title": "View", + "type": "string" + } + }, + "title": "ReviewEnvelope", + "type": "object" + }, + "ReviewFailure:serialization": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A claim outside the issues whose work failed. ``stage`` is\n``assessment`` or ``verification``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "stage": { + "default": "", + "title": "Stage", + "type": "string" + } + }, + "title": "ReviewFailure", + "type": "object" + }, + "ReviewFailure:validation": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A claim outside the issues whose work failed. ``stage`` is\n``assessment`` or ``verification``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "stage": { + "default": "", + "title": "Stage", + "type": "string" + } + }, + "title": "ReviewFailure", + "type": "object" + }, + "ReviewFull:serialization": { + "$defs": { + "ClaimLocation": { + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "EscalationPolicy": { + "additionalProperties": true, + "description": "Which quick-checked claims get a deep check, as the review RESOLVED it.\n\nA claim is deep-checked when its quick verdict is in ``verdicts`` OR its\nconfidence band is in ``confidence``, up to ``max_verifications`` of them,\nat ``depth``. ``max_assessments`` is how many of the draft's claims, most\ncheck-worthy first, got a quick verdict.", + "properties": { + "confidence": { + "items": { + "type": "string" + }, + "title": "Confidence", + "type": "array" + }, + "depth": { + "default": "standard", + "title": "Depth", + "type": "string" + }, + "max_assessments": { + "default": 20, + "title": "Max Assessments", + "type": "integer" + }, + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + }, + "max_verifications": { + "default": 5, + "title": "Max Verifications", + "type": "integer" + }, + "suggest_edits": { + "default": false, + "title": "Suggest Edits", + "type": "boolean" + }, + "verdicts": { + "items": { + "type": "string" + }, + "title": "Verdicts", + "type": "array" + } + }, + "title": "EscalationPolicy", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewAssessment": { + "additionalProperties": true, + "description": "A claim's quick check. ``status``: ``pending`` | ``running`` |\n``completed`` | ``failed``. On ``completed`` the fields mean what they\nmean on an ``/assess`` row; on ``failed`` see ``error_code`` (an open\nset, as on ``/assess``), ``hint`` and ``failure``.", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "ReviewAssessment", + "type": "object" + }, + "ReviewAssessmentCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewCitation": { + "additionalProperties": true, + "description": "One citation of the draft (a URL or a DOI where the draft uses it), in\nthe draft's order. Only on a review that asked for the citation check.\n\n``reference`` is the citation as the draft writes it; ``statement`` the\ndraft's sentence it is attached to, with link syntax reduced to its\nwords; ``quotes`` the words the draft quotes from this source.\n``position`` locates ``statement`` in the text as sent (its ``text`` is\n``None``: the row carries the ``statement``). ``result`` is\n``None`` until the check has ended, and on a failed row with nothing\nestablished.", + "properties": { + "check": { + "$ref": "#/$defs/ReviewCitationCheck" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitation", + "type": "object" + }, + "ReviewCitationCheck": { + "additionalProperties": true, + "description": "A citation's check. ``status`` is progress, as on ``assessment``:\n``pending`` | ``running`` | ``completed`` | ``failed``.\n\n- ``page_read``: ``full``, ``partial``, ``not_found`` or ``none``.\n- ``source_url``: where the text was actually read from (after\n redirects, or a free copy of a paper). ``source_version``, for a DOI:\n ``published``, ``accepted`` or ``submitted``.\n- ``support``: ``supported``, ``partly_supported``, ``contradicted``,\n ``not_in_source`` or ``unchecked``. ``snippet`` is the verified passage\n of the source it rests on; ``rationale`` is a reviewer's note, not a\n checked source.\n- ``quote``: ``matched``, ``not_in_source`` or ``unchecked``; ``None``\n when the draft quoted nothing from this source. ``missing_quote`` is\n the quoted excerpt the quote check did not find; ``None`` unless\n ``quote`` is ``not_in_source``.\n- ``doi_registered``, ``metadata`` (``consistent``, ``mismatch`` or\n ``unchecked``), ``metadata_differences`` and ``registered``: set for a\n DOI only.\n- ``unchecked_reason`` and ``hint``: why the finding is ``unchecked`` and\n what to do next. The reasons are an open set, e.g. ``no_text``,\n ``partial_text``, ``login_required``, ``unsupported_site``,\n ``no_statement``, ``invalid_url``, ``other_version``, ``inconclusive``,\n ``ambiguous_reference``.\n- ``failure``: on a ``failed`` check.", + "properties": { + "doi_registered": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi Registered" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "metadata": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Metadata" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Language" + }, + "page_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Published Date" + }, + "page_read": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Read" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quote" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "registered": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationRecord" + }, + { + "type": "null" + } + ], + "default": null + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Version" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "support": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Support" + }, + "unchecked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unchecked Reason" + } + }, + "title": "ReviewCitationCheck", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationFailure": { + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationIssue": { + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCitationRecord": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + }, + "ReviewCitationResult": { + "additionalProperties": true, + "description": "The one answer to read for a citation, derived from its ``check``.\n\n``finding``, most serious first: ``doi_not_found``, ``page_not_found``,\n``contradicted``, ``quote_not_in_source``, ``not_in_source``,\n``partly_supported``, ``metadata_mismatch`` (each an issue), then\n``supported`` and ``unchecked``. ``source`` says which check the finding\ncame from: ``doi``, ``page``, ``support``, ``quote`` or ``metadata``;\n``None`` on ``unchecked``.", + "properties": { + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + } + }, + "title": "ReviewCitationResult", + "type": "object" + }, + "ReviewClaim": { + "additionalProperties": true, + "description": "One claim of the draft, in the order the draft's claims were read.\n\n``result`` is ``None`` until the quick check completes, and on a failed\none. ``escalation`` is ``None`` until then too. ``verification`` is set\nonce a deep check was planned for the claim.\n\n``positions`` lists every place the draft makes the claim, in text\norder, at most 10 (see ``Position``). ``start`` / ``end`` index the\n``text`` you sent in Unicode code points (``text[start:end]`` in Python;\n``end`` exclusive), the same coordinates as a citation's ``position``.\nFor a URL draft, ``start`` and ``end`` are ``None`` and ``text`` carries\nthe passage. ``positions`` is ``None`` when the claims could not be\nlocated, or once a zero-retention draft is gone (and from servers that\npredate the field).", + "properties": { + "assessment": { + "$ref": "#/$defs/ReviewAssessment" + }, + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "verification": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerification" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewClaim", + "type": "object" + }, + "ReviewCredits": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewEntity": { + "additionalProperties": true, + "description": "An entity the deep check found in the claim. ``qid`` is its Wikidata\nid when resolved. Either may be ``None``.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "ReviewEntity", + "type": "object" + }, + "ReviewFailure": { + "additionalProperties": true, + "description": "A claim outside the issues whose work failed. ``stage`` is\n``assessment`` or ``verification``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "stage": { + "default": "", + "title": "Stage", + "type": "string" + } + }, + "title": "ReviewFailure", + "type": "object" + }, + "ReviewIssue": { + "additionalProperties": true, + "description": "A claim whose final verdict is ``False``, ``Mostly False`` or ``Mixed``.\n\n``source`` says which check the verdict comes from: ``verification`` (a\ndeep check, with ``verification_id``, ``url`` and ``key_finding``) or\n``assessment`` (the quick check only; ``escalation`` says why it was not\ndeep-checked, and ``rationale`` is the reviewers' note).\n``verified_claim`` is the deep check's wording of the claim when it\ndiffers from ``claim``. ``failure`` is set when the claim's deep check\nfailed.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Status" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "verified_claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verified Claim" + } + }, + "title": "ReviewIssue", + "type": "object" + }, + "ReviewMoreCitation": { + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + }, + "ReviewResult": { + "additionalProperties": true, + "description": "The one answer to read for a claim: the deep check's verdict when it\ncompleted, else the quick check's. ``source`` says which\n(``assessment`` or ``verification``).", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + } + }, + "title": "ReviewResult", + "type": "object" + }, + "ReviewSummary": { + "additionalProperties": true, + "description": "Counts over the review. A count is ``None`` until what it counts is\nknown (``claims_selected`` before the draft has been read).\n\nThe ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review\nthat did not ask for the citation check.", + "properties": { + "assessments": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewAssessmentCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + }, + "citations_skipped": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Skipped" + }, + "claim_limit": { + "default": 20, + "title": "Claim Limit", + "type": "integer" + }, + "claim_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim Limit Reached" + }, + "claims_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claims Selected" + }, + "input_truncated": { + "default": false, + "title": "Input Truncated", + "type": "boolean" + }, + "issues": { + "default": 0, + "title": "Issues", + "type": "integer" + }, + "verifications": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerificationCounts" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewSummary", + "type": "object" + }, + "ReviewVerification": { + "additionalProperties": true, + "description": "A claim's deep check. ``status``: ``processing`` | ``completed`` |\n``failed``; while ``processing`` only ``status``, ``content_status`` and\nthe ids are set. ``content_status`` is ``purged`` once the verification\nwas deleted or removed by its account's retention period: the verdict and\nids stay, the text fields and links are ``None``.\n\n``visibility`` and ``depth`` are what the verification actually has: a\nverdict served from an earlier check keeps its own visibility and may be\n``standard`` for a ``low`` request. Sources are on the verification\nitself: ``client.verifications.get(verification_id)``, or ``url``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "content_status": { + "default": "available", + "title": "Content Status", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Depth" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Domain" + }, + "entities": { + "items": { + "$ref": "#/$defs/ReviewEntity" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Executive Summary" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "task_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Task Id" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "visibility": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Visibility" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "ReviewVerification", + "type": "object" + }, + "ReviewVerificationCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "``GET /reviews/{review_id}``: the envelope plus every claim.", + "properties": { + "citation_failures": { + "items": { + "$ref": "#/$defs/ReviewCitationFailure" + }, + "title": "Citation Failures", + "type": "array" + }, + "citation_issues": { + "items": { + "$ref": "#/$defs/ReviewCitationIssue" + }, + "title": "Citation Issues", + "type": "array" + }, + "citations": { + "items": { + "$ref": "#/$defs/ReviewCitation" + }, + "title": "Citations", + "type": "array" + }, + "claims": { + "items": { + "$ref": "#/$defs/ReviewClaim" + }, + "title": "Claims", + "type": "array" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed At" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "credits": { + "$ref": "#/$defs/ReviewCredits" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "failures": { + "items": { + "$ref": "#/$defs/ReviewFailure" + }, + "title": "Failures", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/$defs/ReviewIssue" + }, + "title": "Issues", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ReviewMoreCitation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Citations" + }, + "more_claim_locations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ClaimLocation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claim Locations" + }, + "more_claims": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claims" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Outcome" + }, + "policy": { + "$ref": "#/$defs/EscalationPolicy" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "review_id": { + "default": "", + "title": "Review Id", + "type": "string" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/ReviewSummary" + }, + "view": { + "default": "full", + "title": "View", + "type": "string" + } + }, + "title": "ReviewFull", + "type": "object" + }, + "ReviewFull:validation": { + "$defs": { + "ClaimLocation": { + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "EscalationPolicy": { + "additionalProperties": true, + "description": "Which quick-checked claims get a deep check, as the review RESOLVED it.\n\nA claim is deep-checked when its quick verdict is in ``verdicts`` OR its\nconfidence band is in ``confidence``, up to ``max_verifications`` of them,\nat ``depth``. ``max_assessments`` is how many of the draft's claims, most\ncheck-worthy first, got a quick verdict.", + "properties": { + "confidence": { + "items": { + "type": "string" + }, + "title": "Confidence", + "type": "array" + }, + "depth": { + "default": "standard", + "title": "Depth", + "type": "string" + }, + "max_assessments": { + "default": 20, + "title": "Max Assessments", + "type": "integer" + }, + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + }, + "max_verifications": { + "default": 5, + "title": "Max Verifications", + "type": "integer" + }, + "suggest_edits": { + "default": false, + "title": "Suggest Edits", + "type": "boolean" + }, + "verdicts": { + "items": { + "type": "string" + }, + "title": "Verdicts", + "type": "array" + } + }, + "title": "EscalationPolicy", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewAssessment": { + "additionalProperties": true, + "description": "A claim's quick check. ``status``: ``pending`` | ``running`` |\n``completed`` | ``failed``. On ``completed`` the fields mean what they\nmean on an ``/assess`` row; on ``failed`` see ``error_code`` (an open\nset, as on ``/assess``), ``hint`` and ``failure``.", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "dissent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dissent" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Error Code" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "identified_claims": { + "items": { + "type": "string" + }, + "title": "Identified Claims", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + } + }, + "title": "ReviewAssessment", + "type": "object" + }, + "ReviewAssessmentCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewCitation": { + "additionalProperties": true, + "description": "One citation of the draft (a URL or a DOI where the draft uses it), in\nthe draft's order. Only on a review that asked for the citation check.\n\n``reference`` is the citation as the draft writes it; ``statement`` the\ndraft's sentence it is attached to, with link syntax reduced to its\nwords; ``quotes`` the words the draft quotes from this source.\n``position`` locates ``statement`` in the text as sent (its ``text`` is\n``None``: the row carries the ``statement``). ``result`` is\n``None`` until the check has ended, and on a failed row with nothing\nestablished.", + "properties": { + "check": { + "$ref": "#/$defs/ReviewCitationCheck" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitation", + "type": "object" + }, + "ReviewCitationCheck": { + "additionalProperties": true, + "description": "A citation's check. ``status`` is progress, as on ``assessment``:\n``pending`` | ``running`` | ``completed`` | ``failed``.\n\n- ``page_read``: ``full``, ``partial``, ``not_found`` or ``none``.\n- ``source_url``: where the text was actually read from (after\n redirects, or a free copy of a paper). ``source_version``, for a DOI:\n ``published``, ``accepted`` or ``submitted``.\n- ``support``: ``supported``, ``partly_supported``, ``contradicted``,\n ``not_in_source`` or ``unchecked``. ``snippet`` is the verified passage\n of the source it rests on; ``rationale`` is a reviewer's note, not a\n checked source.\n- ``quote``: ``matched``, ``not_in_source`` or ``unchecked``; ``None``\n when the draft quoted nothing from this source. ``missing_quote`` is\n the quoted excerpt the quote check did not find; ``None`` unless\n ``quote`` is ``not_in_source``.\n- ``doi_registered``, ``metadata`` (``consistent``, ``mismatch`` or\n ``unchecked``), ``metadata_differences`` and ``registered``: set for a\n DOI only.\n- ``unchecked_reason`` and ``hint``: why the finding is ``unchecked`` and\n what to do next. The reasons are an open set, e.g. ``no_text``,\n ``partial_text``, ``login_required``, ``unsupported_site``,\n ``no_statement``, ``invalid_url``, ``other_version``, ``inconclusive``,\n ``ambiguous_reference``.\n- ``failure``: on a ``failed`` check.", + "properties": { + "doi_registered": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi Registered" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "metadata": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Metadata" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Language" + }, + "page_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Published Date" + }, + "page_read": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Read" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quote" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "registered": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationRecord" + }, + { + "type": "null" + } + ], + "default": null + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Version" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "support": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Support" + }, + "unchecked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unchecked Reason" + } + }, + "title": "ReviewCitationCheck", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationFailure": { + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationIssue": { + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCitationRecord": { + "additionalProperties": true, + "description": "A DOI's record in the registry.", + "properties": { + "authors": { + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "journal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Journal" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "year": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Year" + } + }, + "title": "ReviewCitationRecord", + "type": "object" + }, + "ReviewCitationResult": { + "additionalProperties": true, + "description": "The one answer to read for a citation, derived from its ``check``.\n\n``finding``, most serious first: ``doi_not_found``, ``page_not_found``,\n``contradicted``, ``quote_not_in_source``, ``not_in_source``,\n``partly_supported``, ``metadata_mismatch`` (each an issue), then\n``supported`` and ``unchecked``. ``source`` says which check the finding\ncame from: ``doi``, ``page``, ``support``, ``quote`` or ``metadata``;\n``None`` on ``unchecked``.", + "properties": { + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + } + }, + "title": "ReviewCitationResult", + "type": "object" + }, + "ReviewClaim": { + "additionalProperties": true, + "description": "One claim of the draft, in the order the draft's claims were read.\n\n``result`` is ``None`` until the quick check completes, and on a failed\none. ``escalation`` is ``None`` until then too. ``verification`` is set\nonce a deep check was planned for the claim.\n\n``positions`` lists every place the draft makes the claim, in text\norder, at most 10 (see ``Position``). ``start`` / ``end`` index the\n``text`` you sent in Unicode code points (``text[start:end]`` in Python;\n``end`` exclusive), the same coordinates as a citation's ``position``.\nFor a URL draft, ``start`` and ``end`` are ``None`` and ``text`` carries\nthe passage. ``positions`` is ``None`` when the claims could not be\nlocated, or once a zero-retention draft is gone (and from servers that\npredate the field).", + "properties": { + "assessment": { + "$ref": "#/$defs/ReviewAssessment" + }, + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "verification": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerification" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewClaim", + "type": "object" + }, + "ReviewCredits": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewEntity": { + "additionalProperties": true, + "description": "An entity the deep check found in the claim. ``qid`` is its Wikidata\nid when resolved. Either may be ``None``.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "ReviewEntity", + "type": "object" + }, + "ReviewFailure": { + "additionalProperties": true, + "description": "A claim outside the issues whose work failed. ``stage`` is\n``assessment`` or ``verification``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "stage": { + "default": "", + "title": "Stage", + "type": "string" + } + }, + "title": "ReviewFailure", + "type": "object" + }, + "ReviewIssue": { + "additionalProperties": true, + "description": "A claim whose final verdict is ``False``, ``Mostly False`` or ``Mixed``.\n\n``source`` says which check the verdict comes from: ``verification`` (a\ndeep check, with ``verification_id``, ``url`` and ``key_finding``) or\n``assessment`` (the quick check only; ``escalation`` says why it was not\ndeep-checked, and ``rationale`` is the reviewers' note).\n``verified_claim`` is the deep check's wording of the claim when it\ndiffers from ``claim``. ``failure`` is set when the claim's deep check\nfailed.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Status" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "verified_claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verified Claim" + } + }, + "title": "ReviewIssue", + "type": "object" + }, + "ReviewMoreCitation": { + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + }, + "ReviewResult": { + "additionalProperties": true, + "description": "The one answer to read for a claim: the deep check's verdict when it\ncompleted, else the quick check's. ``source`` says which\n(``assessment`` or ``verification``).", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + } + }, + "title": "ReviewResult", + "type": "object" + }, + "ReviewSummary": { + "additionalProperties": true, + "description": "Counts over the review. A count is ``None`` until what it counts is\nknown (``claims_selected`` before the draft has been read).\n\nThe ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review\nthat did not ask for the citation check.", + "properties": { + "assessments": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewAssessmentCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + }, + "citations_skipped": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Skipped" + }, + "claim_limit": { + "default": 20, + "title": "Claim Limit", + "type": "integer" + }, + "claim_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim Limit Reached" + }, + "claims_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claims Selected" + }, + "input_truncated": { + "default": false, + "title": "Input Truncated", + "type": "boolean" + }, + "issues": { + "default": 0, + "title": "Issues", + "type": "integer" + }, + "verifications": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerificationCounts" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewSummary", + "type": "object" + }, + "ReviewVerification": { + "additionalProperties": true, + "description": "A claim's deep check. ``status``: ``processing`` | ``completed`` |\n``failed``; while ``processing`` only ``status``, ``content_status`` and\nthe ids are set. ``content_status`` is ``purged`` once the verification\nwas deleted or removed by its account's retention period: the verdict and\nids stay, the text fields and links are ``None``.\n\n``visibility`` and ``depth`` are what the verification actually has: a\nverdict served from an earlier check keeps its own visibility and may be\n``standard`` for a ``low`` request. Sources are on the verification\nitself: ``client.verifications.get(verification_id)``, or ``url``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "content_status": { + "default": "available", + "title": "Content Status", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Depth" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Domain" + }, + "entities": { + "items": { + "$ref": "#/$defs/ReviewEntity" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Executive Summary" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "task_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Task Id" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "visibility": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Visibility" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "ReviewVerification", + "type": "object" + }, + "ReviewVerificationCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "``GET /reviews/{review_id}``: the envelope plus every claim.", + "properties": { + "citation_failures": { + "items": { + "$ref": "#/$defs/ReviewCitationFailure" + }, + "title": "Citation Failures", + "type": "array" + }, + "citation_issues": { + "items": { + "$ref": "#/$defs/ReviewCitationIssue" + }, + "title": "Citation Issues", + "type": "array" + }, + "citations": { + "items": { + "$ref": "#/$defs/ReviewCitation" + }, + "title": "Citations", + "type": "array" + }, + "claims": { + "items": { + "$ref": "#/$defs/ReviewClaim" + }, + "title": "Claims", + "type": "array" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed At" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "credits": { + "$ref": "#/$defs/ReviewCredits" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "failures": { + "items": { + "$ref": "#/$defs/ReviewFailure" + }, + "title": "Failures", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/$defs/ReviewIssue" + }, + "title": "Issues", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ReviewMoreCitation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Citations" + }, + "more_claim_locations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ClaimLocation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claim Locations" + }, + "more_claims": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claims" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Outcome" + }, + "policy": { + "$ref": "#/$defs/EscalationPolicy" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "review_id": { + "default": "", + "title": "Review Id", + "type": "string" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/ReviewSummary" + }, + "view": { + "default": "full", + "title": "View", + "type": "string" + } + }, + "title": "ReviewFull", + "type": "object" + }, + "ReviewIssue:serialization": { + "$defs": { + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A claim whose final verdict is ``False``, ``Mostly False`` or ``Mixed``.\n\n``source`` says which check the verdict comes from: ``verification`` (a\ndeep check, with ``verification_id``, ``url`` and ``key_finding``) or\n``assessment`` (the quick check only; ``escalation`` says why it was not\ndeep-checked, and ``rationale`` is the reviewers' note).\n``verified_claim`` is the deep check's wording of the claim when it\ndiffers from ``claim``. ``failure`` is set when the claim's deep check\nfailed.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Status" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "verified_claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verified Claim" + } + }, + "title": "ReviewIssue", + "type": "object" + }, + "ReviewIssue:validation": { + "$defs": { + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A claim whose final verdict is ``False``, ``Mostly False`` or ``Mixed``.\n\n``source`` says which check the verdict comes from: ``verification`` (a\ndeep check, with ``verification_id``, ``url`` and ``key_finding``) or\n``assessment`` (the quick check only; ``escalation`` says why it was not\ndeep-checked, and ``rationale`` is the reviewers' note).\n``verified_claim`` is the deep check's wording of the claim when it\ndiffers from ``claim``. ``failure`` is set when the claim's deep check\nfailed.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Status" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "verified_claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verified Claim" + } + }, + "title": "ReviewIssue", + "type": "object" + }, + "ReviewIssues:serialization": { + "$defs": { + "ClaimLocation": { + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "EscalationPolicy": { + "additionalProperties": true, + "description": "Which quick-checked claims get a deep check, as the review RESOLVED it.\n\nA claim is deep-checked when its quick verdict is in ``verdicts`` OR its\nconfidence band is in ``confidence``, up to ``max_verifications`` of them,\nat ``depth``. ``max_assessments`` is how many of the draft's claims, most\ncheck-worthy first, got a quick verdict.", + "properties": { + "confidence": { + "items": { + "type": "string" + }, + "title": "Confidence", + "type": "array" + }, + "depth": { + "default": "standard", + "title": "Depth", + "type": "string" + }, + "max_assessments": { + "default": 20, + "title": "Max Assessments", + "type": "integer" + }, + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + }, + "max_verifications": { + "default": 5, + "title": "Max Verifications", + "type": "integer" + }, + "suggest_edits": { + "default": false, + "title": "Suggest Edits", + "type": "boolean" + }, + "verdicts": { + "items": { + "type": "string" + }, + "title": "Verdicts", + "type": "array" + } + }, + "title": "EscalationPolicy", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewAssessmentCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationFailure": { + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationIssue": { + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCredits": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewFailure": { + "additionalProperties": true, + "description": "A claim outside the issues whose work failed. ``stage`` is\n``assessment`` or ``verification``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "stage": { + "default": "", + "title": "Stage", + "type": "string" + } + }, + "title": "ReviewFailure", + "type": "object" + }, + "ReviewIssue": { + "additionalProperties": true, + "description": "A claim whose final verdict is ``False``, ``Mostly False`` or ``Mixed``.\n\n``source`` says which check the verdict comes from: ``verification`` (a\ndeep check, with ``verification_id``, ``url`` and ``key_finding``) or\n``assessment`` (the quick check only; ``escalation`` says why it was not\ndeep-checked, and ``rationale`` is the reviewers' note).\n``verified_claim`` is the deep check's wording of the claim when it\ndiffers from ``claim``. ``failure`` is set when the claim's deep check\nfailed.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Status" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "verified_claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verified Claim" + } + }, + "title": "ReviewIssue", + "type": "object" + }, + "ReviewMoreCitation": { + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + }, + "ReviewSummary": { + "additionalProperties": true, + "description": "Counts over the review. A count is ``None`` until what it counts is\nknown (``claims_selected`` before the draft has been read).\n\nThe ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review\nthat did not ask for the citation check.", + "properties": { + "assessments": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewAssessmentCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + }, + "citations_skipped": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Skipped" + }, + "claim_limit": { + "default": 20, + "title": "Claim Limit", + "type": "integer" + }, + "claim_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim Limit Reached" + }, + "claims_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claims Selected" + }, + "input_truncated": { + "default": false, + "title": "Input Truncated", + "type": "boolean" + }, + "issues": { + "default": 0, + "title": "Issues", + "type": "integer" + }, + "verifications": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerificationCounts" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewSummary", + "type": "object" + }, + "ReviewVerificationCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "``GET /reviews/{review_id}?view=issues``: the envelope, no ``claims``.", + "properties": { + "citation_failures": { + "items": { + "$ref": "#/$defs/ReviewCitationFailure" + }, + "title": "Citation Failures", + "type": "array" + }, + "citation_issues": { + "items": { + "$ref": "#/$defs/ReviewCitationIssue" + }, + "title": "Citation Issues", + "type": "array" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed At" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "credits": { + "$ref": "#/$defs/ReviewCredits" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "failures": { + "items": { + "$ref": "#/$defs/ReviewFailure" + }, + "title": "Failures", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/$defs/ReviewIssue" + }, + "title": "Issues", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ReviewMoreCitation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Citations" + }, + "more_claim_locations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ClaimLocation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claim Locations" + }, + "more_claims": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claims" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Outcome" + }, + "policy": { + "$ref": "#/$defs/EscalationPolicy" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "review_id": { + "default": "", + "title": "Review Id", + "type": "string" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/ReviewSummary" + }, + "view": { + "default": "issues", + "title": "View", + "type": "string" + } + }, + "title": "ReviewIssues", + "type": "object" + }, + "ReviewIssues:validation": { + "$defs": { + "ClaimLocation": { + "additionalProperties": true, + "description": "Where the submitted text makes one claim.\n\n``claim`` is exactly as in the claim list it belongs to\n(``ExtractedClaims.claim`` / ``identified_claims``, or a review's\n``more_claims``). ``positions`` lists every place the text makes it, in\ntext order, at most 10. It is ``None`` only when that claim could not be\nplaced; on ``/extract`` every returned claim is placed.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "positions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Position" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Positions" + } + }, + "title": "ClaimLocation", + "type": "object" + }, + "Escalation": { + "additionalProperties": true, + "description": "Why a claim was, or was not, deep-checked.\n\n``matched_rules`` lists the rules the quick check matched (``verdict``,\n``confidence``, both, or neither). ``disposition`` says what happened:\n``planned`` (deep-checked), ``not_selected`` (no rule matched), ``cap``\n(matched, but ``max_verifications`` was reached), ``credits`` (matched,\nbut the balance ran out) or ``account_cap`` (matched, but the account's\nallowance of concurrent deep checks was reached). To deep-check the\n``cap`` rows yourself, send their claims to ``verify_batch_and_wait``.", + "properties": { + "disposition": { + "default": "", + "title": "Disposition", + "type": "string" + }, + "matched_rules": { + "items": { + "type": "string" + }, + "title": "Matched Rules", + "type": "array" + } + }, + "title": "Escalation", + "type": "object" + }, + "EscalationPolicy": { + "additionalProperties": true, + "description": "Which quick-checked claims get a deep check, as the review RESOLVED it.\n\nA claim is deep-checked when its quick verdict is in ``verdicts`` OR its\nconfidence band is in ``confidence``, up to ``max_verifications`` of them,\nat ``depth``. ``max_assessments`` is how many of the draft's claims, most\ncheck-worthy first, got a quick verdict.", + "properties": { + "confidence": { + "items": { + "type": "string" + }, + "title": "Confidence", + "type": "array" + }, + "depth": { + "default": "standard", + "title": "Depth", + "type": "string" + }, + "max_assessments": { + "default": 20, + "title": "Max Assessments", + "type": "integer" + }, + "max_citations": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Citations" + }, + "max_verifications": { + "default": 5, + "title": "Max Verifications", + "type": "integer" + }, + "suggest_edits": { + "default": false, + "title": "Suggest Edits", + "type": "boolean" + }, + "verdicts": { + "items": { + "type": "string" + }, + "title": "Verdicts", + "type": "array" + } + }, + "title": "EscalationPolicy", + "type": "object" + }, + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + }, + "ReviewAssessmentCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewCitationDifference": { + "additionalProperties": true, + "description": "One way the reference differs from the registry's record. ``field``\nis ``title``, ``authors``, ``year`` or ``journal``.", + "properties": { + "cited": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited" + }, + "field": { + "default": "", + "title": "Field", + "type": "string" + }, + "registered": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registered" + } + }, + "title": "ReviewCitationDifference", + "type": "object" + }, + "ReviewCitationFailure": { + "additionalProperties": true, + "description": "A citation whose check failed with nothing established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + } + }, + "title": "ReviewCitationFailure", + "type": "object" + }, + "ReviewCitationIssue": { + "additionalProperties": true, + "description": "A citation whose finding is an issue (see ``ReviewCitationResult``);\nmost serious first, then in the draft's order.\n\n``snippet`` and ``rationale`` are set only when ``source`` is\n``support``. On ``quote_not_in_source``, ``missing_quote`` is the quoted\nexcerpt that was not found. On ``metadata_mismatch``,\n``metadata_differences`` says what differs. ``failure`` is set when another part of the check failed after\nthe finding was established.", + "properties": { + "citation_index": { + "default": 0, + "title": "Citation Index", + "type": "integer" + }, + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "finding": { + "default": "", + "title": "Finding", + "type": "string" + }, + "metadata_differences": { + "items": { + "$ref": "#/$defs/ReviewCitationDifference" + }, + "title": "Metadata Differences", + "type": "array" + }, + "missing_quote": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Missing Quote" + }, + "page_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Page Title" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "quotes": { + "items": { + "type": "string" + }, + "title": "Quotes", + "type": "array" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snippet" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "statement": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Statement" + } + }, + "title": "ReviewCitationIssue", + "type": "object" + }, + "ReviewCredits": { + "additionalProperties": true, + "description": "``charged`` is the net credits the review cost the account, its\ncitation checks included. It is final once no deep check or citation\ncheck is running: read it at ``completed``.", + "properties": { + "charged": { + "default": 0, + "title": "Charged", + "type": "integer" + } + }, + "title": "ReviewCredits", + "type": "object" + }, + "ReviewFailure": { + "additionalProperties": true, + "description": "A claim outside the issues whose work failed. ``stage`` is\n``assessment`` or ``verification``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "stage": { + "default": "", + "title": "Stage", + "type": "string" + } + }, + "title": "ReviewFailure", + "type": "object" + }, + "ReviewIssue": { + "additionalProperties": true, + "description": "A claim whose final verdict is ``False``, ``Mostly False`` or ``Mixed``.\n\n``source`` says which check the verdict comes from: ``verification`` (a\ndeep check, with ``verification_id``, ``url`` and ``key_finding``) or\n``assessment`` (the quick check only; ``escalation`` says why it was not\ndeep-checked, and ``rationale`` is the reviewers' note).\n``verified_claim`` is the deep check's wording of the claim when it\ndiffers from ``claim``. ``failure`` is set when the claim's deep check\nfailed.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "claim_index": { + "default": 0, + "title": "Claim Index", + "type": "integer" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "escalation": { + "anyOf": [ + { + "$ref": "#/$defs/Escalation" + }, + { + "type": "null" + } + ], + "default": null + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rationale" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "suggested_edits": { + "anyOf": [ + { + "$ref": "#/$defs/SuggestedEdits" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Status" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "verified_claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verified Claim" + } + }, + "title": "ReviewIssue", + "type": "object" + }, + "ReviewMoreCitation": { + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + }, + "ReviewSummary": { + "additionalProperties": true, + "description": "Counts over the review. A count is ``None`` until what it counts is\nknown (``claims_selected`` before the draft has been read).\n\nThe ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review\nthat did not ask for the citation check.", + "properties": { + "assessments": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewAssessmentCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + }, + "citations_skipped": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Skipped" + }, + "claim_limit": { + "default": 20, + "title": "Claim Limit", + "type": "integer" + }, + "claim_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim Limit Reached" + }, + "claims_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claims Selected" + }, + "input_truncated": { + "default": false, + "title": "Input Truncated", + "type": "boolean" + }, + "issues": { + "default": 0, + "title": "Issues", + "type": "integer" + }, + "verifications": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerificationCounts" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewSummary", + "type": "object" + }, + "ReviewVerificationCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + }, + "SuggestedEdit": { + "additionalProperties": true, + "description": "One replacement in the draft: the span ``start``..``end`` of the\n``text`` you sent (Unicode code points, ``end`` exclusive, as in\n``Position``), ``text`` exactly that slice, and ``replacement`` what takes\nits place (``\"\"`` deletes it). ``position`` is the index of the claim\nrow's ``positions`` entry the edit sits in, for grouping by passage.\n\nCompare ``text`` with your draft before you apply an edit, so a draft that\nchanged since is never edited in the wrong place.", + "properties": { + "end": { + "default": 0, + "title": "End", + "type": "integer" + }, + "position": { + "default": 0, + "title": "Position", + "type": "integer" + }, + "replacement": { + "default": "", + "title": "Replacement", + "type": "string" + }, + "start": { + "default": 0, + "title": "Start", + "type": "integer" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "SuggestedEdit", + "type": "object" + }, + "SuggestedEdits": { + "additionalProperties": true, + "description": "The smallest edits to the draft that make it say what the claim's\n``suggested_rewrite`` says (the deep check's, or the quick check's on a\nclaim that stayed on the quick verdict), in the draft's own language\n(``review(..., suggest_edits=True)``).\n\n``status`` is ``\"pending\"`` while they are computed (``edits`` is\n``None``; keep polling) and ``\"completed\"`` once settled, which includes\nsettling on none (``edits == []``: no edit could be made safely, or it\ncould not be computed). They are not themselves verified:\nreview them before you publish.", + "properties": { + "edits": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SuggestedEdit" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Edits" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + } + }, + "title": "SuggestedEdits", + "type": "object" + } + }, + "additionalProperties": true, + "description": "``GET /reviews/{review_id}?view=issues``: the envelope, no ``claims``.", + "properties": { + "citation_failures": { + "items": { + "$ref": "#/$defs/ReviewCitationFailure" + }, + "title": "Citation Failures", + "type": "array" + }, + "citation_issues": { + "items": { + "$ref": "#/$defs/ReviewCitationIssue" + }, + "title": "Citation Issues", + "type": "array" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed At" + }, + "created_at": { + "default": "", + "title": "Created At", + "type": "string" + }, + "credits": { + "$ref": "#/$defs/ReviewCredits" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "failures": { + "items": { + "$ref": "#/$defs/ReviewFailure" + }, + "title": "Failures", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/$defs/ReviewIssue" + }, + "title": "Issues", + "type": "array" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ReviewMoreCitation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Citations" + }, + "more_claim_locations": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ClaimLocation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claim Locations" + }, + "more_claims": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "More Claims" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Outcome" + }, + "policy": { + "$ref": "#/$defs/EscalationPolicy" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "review_id": { + "default": "", + "title": "Review Id", + "type": "string" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/ReviewSummary" + }, + "view": { + "default": "issues", + "title": "View", + "type": "string" + } + }, + "title": "ReviewIssues", + "type": "object" + }, + "ReviewMoreCitation:serialization": { + "$defs": { + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + }, + "ReviewMoreCitation:validation": { + "$defs": { + "Position": { + "additionalProperties": true, + "description": "One place in the text you sent: a claim's passage, or a citation's\nstatement.\n\n``start`` and ``end`` index the ``text`` you sent, in Unicode code\npoints (not UTF-16 units or bytes), half-open: ``text[start:end]`` is\nthe passage in Python, ``end`` exclusive. Both are ``None`` when the\ninput was a URL: the page is not returned, so there is nothing to index,\nand ``text`` carries the passage instead.\n\n``text`` is the passage as it appears in the text. It is ``None`` on a\ncitation's ``position``, whose row already carries the ``statement``.", + "properties": { + "end": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End" + }, + "start": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + } + }, + "title": "Position", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A citation found in the draft past the ones this review checked:\nfound but not checked. Send it in a later request to check it.\n``sentence`` is the draft's sentence around it.", + "properties": { + "cited_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cited Url" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Doi" + }, + "index": { + "default": 0, + "title": "Index", + "type": "integer" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "default": null + }, + "reference": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reference" + }, + "sentence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sentence" + } + }, + "title": "ReviewMoreCitation", + "type": "object" + }, + "ReviewResult:serialization": { + "additionalProperties": true, + "description": "The one answer to read for a claim: the deep check's verdict when it\ncompleted, else the quick check's. ``source`` says which\n(``assessment`` or ``verification``).", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + } + }, + "title": "ReviewResult", + "type": "object" + }, + "ReviewResult:validation": { + "additionalProperties": true, + "description": "The one answer to read for a claim: the deep check's verdict when it\ncompleted, else the quick check's. ``source`` says which\n(``assessment`` or ``verification``).", + "properties": { + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "is_issue": { + "default": false, + "title": "Is Issue", + "type": "boolean" + }, + "source": { + "default": "", + "title": "Source", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + } + }, + "title": "ReviewResult", + "type": "object" + }, + "ReviewStarted:serialization": { + "additionalProperties": true, + "description": "Returned by ``POST /review`` (HTTP 202).\n\n``status`` is always ``\"queued\"``: the acceptance receipt, not the current\nstate. Read the review with ``client.get_review(review_id)``.", + "properties": { + "review_id": { + "default": "", + "title": "Review Id", + "type": "string" + }, + "status": { + "default": "queued", + "title": "Status", + "type": "string" + } + }, + "title": "ReviewStarted", + "type": "object" + }, + "ReviewStarted:validation": { + "additionalProperties": true, + "description": "Returned by ``POST /review`` (HTTP 202).\n\n``status`` is always ``\"queued\"``: the acceptance receipt, not the current\nstate. Read the review with ``client.get_review(review_id)``.", + "properties": { + "review_id": { + "default": "", + "title": "Review Id", + "type": "string" + }, + "status": { + "default": "queued", + "title": "Status", + "type": "string" + } + }, + "title": "ReviewStarted", + "type": "object" + }, + "ReviewSummary:serialization": { + "$defs": { + "ReviewAssessmentCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewVerificationCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Counts over the review. A count is ``None`` until what it counts is\nknown (``claims_selected`` before the draft has been read).\n\nThe ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review\nthat did not ask for the citation check.", + "properties": { + "assessments": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewAssessmentCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + }, + "citations_skipped": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Skipped" + }, + "claim_limit": { + "default": 20, + "title": "Claim Limit", + "type": "integer" + }, + "claim_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim Limit Reached" + }, + "claims_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claims Selected" + }, + "input_truncated": { + "default": false, + "title": "Input Truncated", + "type": "boolean" + }, + "issues": { + "default": 0, + "title": "Issues", + "type": "integer" + }, + "verifications": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerificationCounts" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewSummary", + "type": "object" + }, + "ReviewSummary:validation": { + "$defs": { + "ReviewAssessmentCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + } + }, + "title": "ReviewAssessmentCounts", + "type": "object" + }, + "ReviewCitationCheckCounts": { + "additionalProperties": true, + "description": "Three disjoint counts over the citation rows. ``checked``: a finding\nother than ``unchecked``. ``unchecked``: could not be checked, for a\nreason of the page or the draft. ``failed``: no finding, for a reason of\nours. A row still running is in none of the three.", + "properties": { + "checked": { + "default": 0, + "title": "Checked", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "unchecked": { + "default": 0, + "title": "Unchecked", + "type": "integer" + } + }, + "title": "ReviewCitationCheckCounts", + "type": "object" + }, + "ReviewVerificationCounts": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Counts over the review. A count is ``None`` until what it counts is\nknown (``claims_selected`` before the draft has been read).\n\nThe ``citation*`` counts are ``None`` (``citation_issues`` 0) on a review\nthat did not ask for the citation check.", + "properties": { + "assessments": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewAssessmentCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_checks": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewCitationCheckCounts" + }, + { + "type": "null" + } + ], + "default": null + }, + "citation_issues": { + "default": 0, + "title": "Citation Issues", + "type": "integer" + }, + "citation_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit" + }, + "citation_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citation Limit Reached" + }, + "citations_found": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Found" + }, + "citations_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Selected" + }, + "citations_skipped": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Citations Skipped" + }, + "claim_limit": { + "default": 20, + "title": "Claim Limit", + "type": "integer" + }, + "claim_limit_reached": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim Limit Reached" + }, + "claims_selected": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claims Selected" + }, + "input_truncated": { + "default": false, + "title": "Input Truncated", + "type": "boolean" + }, + "issues": { + "default": 0, + "title": "Issues", + "type": "integer" + }, + "verifications": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewVerificationCounts" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "ReviewSummary", + "type": "object" + }, + "ReviewVerification:serialization": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "ReviewEntity": { + "additionalProperties": true, + "description": "An entity the deep check found in the claim. ``qid`` is its Wikidata\nid when resolved. Either may be ``None``.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "ReviewEntity", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A claim's deep check. ``status``: ``processing`` | ``completed`` |\n``failed``; while ``processing`` only ``status``, ``content_status`` and\nthe ids are set. ``content_status`` is ``purged`` once the verification\nwas deleted or removed by its account's retention period: the verdict and\nids stay, the text fields and links are ``None``.\n\n``visibility`` and ``depth`` are what the verification actually has: a\nverdict served from an earlier check keeps its own visibility and may be\n``standard`` for a ``low`` request. Sources are on the verification\nitself: ``client.verifications.get(verification_id)``, or ``url``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "content_status": { + "default": "available", + "title": "Content Status", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Depth" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Domain" + }, + "entities": { + "items": { + "$ref": "#/$defs/ReviewEntity" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Executive Summary" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "task_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Task Id" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "visibility": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Visibility" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "ReviewVerification", + "type": "object" + }, + "ReviewVerification:validation": { + "$defs": { + "FailureBlock": { + "additionalProperties": true, + "description": "Why a review, or one claim's work inside it, failed.\n\nThe same fields a failed ``GET /verify/status`` carries: ``failure_reason``\nis the specific cause (an open set, e.g. ``no_claim``,\n``insufficient_credits``, ``timeout``), ``failure_class`` the closed\n:data:`FailureClass`, ``retryable`` whether resending the same request can\nhelp, ``hint`` one sentence on what to do next and ``docs_url`` where the\nclass is explained.", + "properties": { + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Docs Url" + }, + "failure_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Class" + }, + "failure_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Failure Reason" + }, + "hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hint" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + } + }, + "title": "FailureBlock", + "type": "object" + }, + "ReviewEntity": { + "additionalProperties": true, + "description": "An entity the deep check found in the claim. ``qid`` is its Wikidata\nid when resolved. Either may be ``None``.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "ReviewEntity", + "type": "object" + } + }, + "additionalProperties": true, + "description": "A claim's deep check. ``status``: ``processing`` | ``completed`` |\n``failed``; while ``processing`` only ``status``, ``content_status`` and\nthe ids are set. ``content_status`` is ``purged`` once the verification\nwas deleted or removed by its account's retention period: the verdict and\nids stay, the text fields and links are ``None``.\n\n``visibility`` and ``depth`` are what the verification actually has: a\nverdict served from an earlier check keeps its own visibility and may be\n``standard`` for a ``low`` request. Sources are on the verification\nitself: ``client.verifications.get(verification_id)``, or ``url``.", + "properties": { + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim" + }, + "confidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "content_status": { + "default": "available", + "title": "Content Status", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Depth" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Domain" + }, + "entities": { + "items": { + "$ref": "#/$defs/ReviewEntity" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Executive Summary" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/$defs/FailureBlock" + }, + { + "type": "null" + } + ], + "default": null + }, + "key_finding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Key Finding" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "task_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Task Id" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verdict" + }, + "verification_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Id" + }, + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Url" + }, + "visibility": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Visibility" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "ReviewVerification", + "type": "object" + }, + "ReviewVerificationCounts:serialization": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + }, + "ReviewVerificationCounts:validation": { + "additionalProperties": true, + "properties": { + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "planned": { + "default": 0, + "title": "Planned", + "type": "integer" + } + }, + "title": "ReviewVerificationCounts", + "type": "object" + }, + "SimilarVerification:serialization": { + "additionalProperties": true, + "description": "An existing public verification that semantically resembles the submitted text.\n\nSame vocabulary as ``Verification`` \u2014 flat ``verdict`` / ``confidence`` /\n``lenz_score`` at top level, no nested ``Verdict`` object.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "distance": { + "default": 0.0, + "title": "Distance", + "type": "number" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "SimilarVerification", + "type": "object" + }, + "SimilarVerification:validation": { + "additionalProperties": true, + "description": "An existing public verification that semantically resembles the submitted text.\n\nSame vocabulary as ``Verification`` \u2014 flat ``verdict`` / ``confidence`` /\n``lenz_score`` at top level, no nested ``Verdict`` object.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "distance": { + "default": 0.0, + "title": "Distance", + "type": "number" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "SimilarVerification", + "type": "object" + }, + "Source:serialization": { + "additionalProperties": true, + "description": "A single citation backing a verification.", + "properties": { + "date": { + "default": "", + "title": "Date", + "type": "string" + }, + "snippet": { + "default": "", + "title": "Snippet", + "type": "string" + }, + "source_name": { + "default": "", + "title": "Source Name", + "type": "string" + }, + "title": { + "default": "", + "title": "Title", + "type": "string" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + } + }, + "title": "Source", + "type": "object" + }, + "Source:validation": { + "additionalProperties": true, + "description": "A single citation backing a verification.", + "properties": { + "date": { + "default": "", + "title": "Date", + "type": "string" + }, + "snippet": { + "default": "", + "title": "Snippet", + "type": "string" + }, + "source_name": { + "default": "", + "title": "Source Name", + "type": "string" + }, + "title": { + "default": "", + "title": "Title", + "type": "string" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + } + }, + "title": "Source", + "type": "object" + }, + "TaskAccepted:serialization": { + "additionalProperties": true, + "description": "Returned by ``POST /verify`` and per item of ``POST /verify/batch``.", + "properties": { + "claim_text": { + "default": "", + "title": "Claim Text", + "type": "string" + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + } + }, + "title": "TaskAccepted", + "type": "object" + }, + "TaskAccepted:validation": { + "additionalProperties": true, + "description": "Returned by ``POST /verify`` and per item of ``POST /verify/batch``.", + "properties": { + "claim_text": { + "default": "", + "title": "Claim Text", + "type": "string" + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + } + }, + "title": "TaskAccepted", + "type": "object" + }, + "TaskStatus:serialization": { + "$defs": { + "Assessment": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "Audit": { + "additionalProperties": true, + "description": "Nested explainability block \u2014 for callers who want the panel's work.", + "properties": { + "adjudication_summary": { + "default": "", + "title": "Adjudication Summary", + "type": "string" + }, + "assessments": { + "items": { + "$ref": "#/$defs/Assessment" + }, + "title": "Assessments", + "type": "array" + }, + "debate_con": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "debate_pro": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "panel_agreement": { + "default": "", + "title": "Panel Agreement", + "type": "string" + } + }, + "title": "Audit", + "type": "object" + }, + "CandidateClaim": { + "additionalProperties": true, + "description": "One of multiple distinct claims framing found in the submitted text.", + "properties": { + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "CandidateClaim", + "type": "object" + }, + "Coverage": { + "additionalProperties": true, + "description": "Whether this verdict carries Lenz's warranty, for YOUR account.\n\nPresent only when covered verification is enabled on the account's plan;\n``None`` otherwise. It describes the pair (your account, this analysis),\nnot the analysis alone \u2014 two accounts can hold two certificates over one\ncached verdict, so the block you see is never another customer's.\n\n``reasons`` is empty exactly when ``status`` is not ``\"uncovered\"``.\n\nThe money fields are three, not two: ``currency`` is ISO 4217 and the\namounts are integers in **major units** \u2014 ``cap=10000`` means ten\nthousand, not a hundred. They are contract figures, not amounts a payment\nprocessor charges. Read ``currency``; do not assume EUR.", + "properties": { + "aggregate": { + "default": 0, + "title": "Aggregate", + "type": "integer" + }, + "as_of": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "As Of" + }, + "cap": { + "default": 0, + "title": "Cap", + "type": "integer" + }, + "certificate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Id" + }, + "certificate_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Url" + }, + "currency": { + "default": "", + "title": "Currency", + "type": "string" + }, + "reasons": { + "items": { + "type": "string" + }, + "title": "Reasons", + "type": "array" + }, + "status": { + "default": "uncovered", + "title": "Status", + "type": "string" + }, + "terms_version": { + "default": "", + "title": "Terms Version", + "type": "string" + } + }, + "title": "Coverage", + "type": "object" + }, + "DebateSide": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + }, + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "Progress": { + "additionalProperties": true, + "description": "Where a running verification has got to. Advisory \u2014 never results.\n\n``step`` is one of ``starting`` / ``framing`` / ``research`` / ``debate``\n/ ``adjudication`` / ``conclusion``. ``index`` is the 1-based stage\nposition (0 while ``starting``) out of ``total``; read ``total`` off the\nresponse rather than hard-coding it. ``poll_after_seconds`` is how long\nto wait before looking again \u2014 :meth:`Lenz.verify_and_wait` honours it\nfor you. ``elapsed_seconds`` is how long the run has been going, a\nmeasurement rather than an estimate of what is left.\n\n``index`` is stage POSITION, not elapsed work: the stages are uneven, so\na bar driven by it sits on ``research`` for roughly half the run.\n\n``step`` stays a plain ``str``, not a ``Literal`` \u2014 an SDK that\nhard-rejects a stage name the server adds later is worse than one that\npasses it through.\n\nThis was a plain ``dict`` up to 2.10.0. The mapping methods below keep\n``p[\"step\"]``, ``\"step\" in p``, ``p.keys()`` and ``dict(p)`` working on a\n2.x minor; they are deprecated and go in 3.0.0. Use attributes.", + "properties": { + "elapsed_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Elapsed Seconds" + }, + "index": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Index" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "step": { + "default": "", + "title": "Step", + "type": "string" + }, + "total": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Total" + } + }, + "title": "Progress", + "type": "object" + }, + "SimilarVerification": { + "additionalProperties": true, + "description": "An existing public verification that semantically resembles the submitted text.\n\nSame vocabulary as ``Verification`` \u2014 flat ``verdict`` / ``confidence`` /\n``lenz_score`` at top level, no nested ``Verdict`` object.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "distance": { + "default": 0.0, + "title": "Distance", + "type": "number" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "SimilarVerification", + "type": "object" + }, + "Source": { + "additionalProperties": true, + "description": "A single citation backing a verification.", + "properties": { + "date": { + "default": "", + "title": "Date", + "type": "string" + }, + "snippet": { + "default": "", + "title": "Snippet", + "type": "string" + }, + "source_name": { + "default": "", + "title": "Source Name", + "type": "string" + }, + "title": { + "default": "", + "title": "Title", + "type": "string" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + } + }, + "title": "Source", + "type": "object" + }, + "Verification": { + "additionalProperties": true, + "description": "Full verification report \u2014 returned by ``verify_and_wait``,\n``verifications.get``, the ``/verify/status/{task_id}`` polling\nendpoint, and the webhook payload.\n\nThe verdict block is FLAT at top level (was nested ``Verdict`` object\npre-unify). ``created_at`` + ``modified_at`` are the only timestamp\nfields on the API surface \u2014 editorial ``published_at`` is internal-only.\n\n1.1.0: dropped ``url`` and ``visibility``. API claims are private by\ndefault and referenced by ``verification_id`` only. Cache-hit on\nanother customer's claim is transparent \u2014 the customer always sees\ntheir own ``verification_id``, never another customer's.\n\nLater: ``visibility`` returns \u2014 'private' | 'unlisted' | 'public'. It\nechoes what you set on submit ('private'/'unlisted' are settable;\n'public' can only be read, for listed claims). ``url`` stays dropped.", + "properties": { + "audit": { + "$ref": "#/$defs/Audit" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "coverage": { + "anyOf": [ + { + "$ref": "#/$defs/Coverage" + }, + { + "type": "null" + } + ], + "default": null + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "default": "", + "title": "Depth", + "type": "string" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "presumed_intent": { + "default": "", + "title": "Presumed Intent", + "type": "string" + }, + "sources": { + "items": { + "$ref": "#/$defs/Source" + }, + "title": "Sources", + "type": "array" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + }, + "visibility": { + "default": "", + "title": "Visibility", + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Verification", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Returned by ``GET /verify/status/{task_id}``.", + "properties": { + "candidates": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidates", + "type": "array" + }, + "claims": { + "items": { + "$ref": "#/$defs/CandidateClaim" + }, + "title": "Claims", + "type": "array" + }, + "docs_url": { + "default": "", + "title": "Docs Url", + "type": "string" + }, + "error": { + "default": "", + "title": "Error", + "type": "string" + }, + "failure_class": { + "default": "", + "title": "Failure Class", + "type": "string" + }, + "failure_detail": { + "default": "", + "title": "Failure Detail", + "type": "string" + }, + "failure_reason": { + "default": "", + "title": "Failure Reason", + "type": "string" + }, + "hint": { + "default": "", + "title": "Hint", + "type": "string" + }, + "progress": { + "$ref": "#/$defs/Progress" + }, + "reason": { + "default": "", + "title": "Reason", + "type": "string" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/Verification" + }, + { + "type": "null" + } + ], + "default": null + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "similar_claims": { + "deprecated": true, + "items": { + "$ref": "#/$defs/SimilarVerification" + }, + "title": "Similar Claims", + "type": "array" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + } + }, + "title": "TaskStatus", + "type": "object" + }, + "TaskStatus:validation": { + "$defs": { + "Assessment": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "Audit": { + "additionalProperties": true, + "description": "Nested explainability block \u2014 for callers who want the panel's work.", + "properties": { + "adjudication_summary": { + "default": "", + "title": "Adjudication Summary", + "type": "string" + }, + "assessments": { + "items": { + "$ref": "#/$defs/Assessment" + }, + "title": "Assessments", + "type": "array" + }, + "debate_con": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "debate_pro": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "panel_agreement": { + "default": "", + "title": "Panel Agreement", + "type": "string" + } + }, + "title": "Audit", + "type": "object" + }, + "CandidateClaim": { + "additionalProperties": true, + "description": "One of multiple distinct claims framing found in the submitted text.", + "properties": { + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "text": { + "default": "", + "title": "Text", + "type": "string" + } + }, + "title": "CandidateClaim", + "type": "object" + }, + "Coverage": { + "additionalProperties": true, + "description": "Whether this verdict carries Lenz's warranty, for YOUR account.\n\nPresent only when covered verification is enabled on the account's plan;\n``None`` otherwise. It describes the pair (your account, this analysis),\nnot the analysis alone \u2014 two accounts can hold two certificates over one\ncached verdict, so the block you see is never another customer's.\n\n``reasons`` is empty exactly when ``status`` is not ``\"uncovered\"``.\n\nThe money fields are three, not two: ``currency`` is ISO 4217 and the\namounts are integers in **major units** \u2014 ``cap=10000`` means ten\nthousand, not a hundred. They are contract figures, not amounts a payment\nprocessor charges. Read ``currency``; do not assume EUR.", + "properties": { + "aggregate": { + "default": 0, + "title": "Aggregate", + "type": "integer" + }, + "as_of": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "As Of" + }, + "cap": { + "default": 0, + "title": "Cap", + "type": "integer" + }, + "certificate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Id" + }, + "certificate_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Url" + }, + "currency": { + "default": "", + "title": "Currency", + "type": "string" + }, + "reasons": { + "items": { + "type": "string" + }, + "title": "Reasons", + "type": "array" + }, + "status": { + "default": "uncovered", + "title": "Status", + "type": "string" + }, + "terms_version": { + "default": "", + "title": "Terms Version", + "type": "string" + } + }, + "title": "Coverage", + "type": "object" + }, + "DebateSide": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + }, + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "Progress": { + "additionalProperties": true, + "description": "Where a running verification has got to. Advisory \u2014 never results.\n\n``step`` is one of ``starting`` / ``framing`` / ``research`` / ``debate``\n/ ``adjudication`` / ``conclusion``. ``index`` is the 1-based stage\nposition (0 while ``starting``) out of ``total``; read ``total`` off the\nresponse rather than hard-coding it. ``poll_after_seconds`` is how long\nto wait before looking again \u2014 :meth:`Lenz.verify_and_wait` honours it\nfor you. ``elapsed_seconds`` is how long the run has been going, a\nmeasurement rather than an estimate of what is left.\n\n``index`` is stage POSITION, not elapsed work: the stages are uneven, so\na bar driven by it sits on ``research`` for roughly half the run.\n\n``step`` stays a plain ``str``, not a ``Literal`` \u2014 an SDK that\nhard-rejects a stage name the server adds later is worse than one that\npasses it through.\n\nThis was a plain ``dict`` up to 2.10.0. The mapping methods below keep\n``p[\"step\"]``, ``\"step\" in p``, ``p.keys()`` and ``dict(p)`` working on a\n2.x minor; they are deprecated and go in 3.0.0. Use attributes.", + "properties": { + "elapsed_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Elapsed Seconds" + }, + "index": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Index" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Poll After Seconds" + }, + "step": { + "default": "", + "title": "Step", + "type": "string" + }, + "total": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Total" + } + }, + "title": "Progress", + "type": "object" + }, + "SimilarVerification": { + "additionalProperties": true, + "description": "An existing public verification that semantically resembles the submitted text.\n\nSame vocabulary as ``Verification`` \u2014 flat ``verdict`` / ``confidence`` /\n``lenz_score`` at top level, no nested ``Verdict`` object.", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "distance": { + "default": 0.0, + "title": "Distance", + "type": "number" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "SimilarVerification", + "type": "object" + }, + "Source": { + "additionalProperties": true, + "description": "A single citation backing a verification.", + "properties": { + "date": { + "default": "", + "title": "Date", + "type": "string" + }, + "snippet": { + "default": "", + "title": "Snippet", + "type": "string" + }, + "source_name": { + "default": "", + "title": "Source Name", + "type": "string" + }, + "title": { + "default": "", + "title": "Title", + "type": "string" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + } + }, + "title": "Source", + "type": "object" + }, + "Verification": { + "additionalProperties": true, + "description": "Full verification report \u2014 returned by ``verify_and_wait``,\n``verifications.get``, the ``/verify/status/{task_id}`` polling\nendpoint, and the webhook payload.\n\nThe verdict block is FLAT at top level (was nested ``Verdict`` object\npre-unify). ``created_at`` + ``modified_at`` are the only timestamp\nfields on the API surface \u2014 editorial ``published_at`` is internal-only.\n\n1.1.0: dropped ``url`` and ``visibility``. API claims are private by\ndefault and referenced by ``verification_id`` only. Cache-hit on\nanother customer's claim is transparent \u2014 the customer always sees\ntheir own ``verification_id``, never another customer's.\n\nLater: ``visibility`` returns \u2014 'private' | 'unlisted' | 'public'. It\nechoes what you set on submit ('private'/'unlisted' are settable;\n'public' can only be read, for listed claims). ``url`` stays dropped.", + "properties": { + "audit": { + "$ref": "#/$defs/Audit" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "coverage": { + "anyOf": [ + { + "$ref": "#/$defs/Coverage" + }, + { + "type": "null" + } + ], + "default": null + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "default": "", + "title": "Depth", + "type": "string" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "presumed_intent": { + "default": "", + "title": "Presumed Intent", + "type": "string" + }, + "sources": { + "items": { + "$ref": "#/$defs/Source" + }, + "title": "Sources", + "type": "array" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + }, + "visibility": { + "default": "", + "title": "Visibility", + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Verification", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Returned by ``GET /verify/status/{task_id}``.", + "properties": { + "candidates": { + "deprecated": true, + "items": { + "type": "string" + }, + "title": "Candidates", + "type": "array" + }, + "claims": { + "items": { + "$ref": "#/$defs/CandidateClaim" + }, + "title": "Claims", + "type": "array" + }, + "docs_url": { + "default": "", + "title": "Docs Url", + "type": "string" + }, + "error": { + "default": "", + "title": "Error", + "type": "string" + }, + "failure_class": { + "default": "", + "title": "Failure Class", + "type": "string" + }, + "failure_detail": { + "default": "", + "title": "Failure Detail", + "type": "string" + }, + "failure_reason": { + "default": "", + "title": "Failure Reason", + "type": "string" + }, + "hint": { + "default": "", + "title": "Hint", + "type": "string" + }, + "progress": { + "$ref": "#/$defs/Progress" + }, + "reason": { + "default": "", + "title": "Reason", + "type": "string" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/Verification" + }, + { + "type": "null" + } + ], + "default": null + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "similar_claims": { + "deprecated": true, + "items": { + "$ref": "#/$defs/SimilarVerification" + }, + "title": "Similar Claims", + "type": "array" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "task_id": { + "default": "", + "title": "Task Id", + "type": "string" + } + }, + "title": "TaskStatus", + "type": "object" + }, + "Usage:serialization": { + "$defs": { + "UsageCapacity": { + "additionalProperties": true, + "description": "DEPRECATED. One capability's share of the pool, in that capability's unit.\n\n**Removed 2026-11-29**, together with the per-block ``credits`` alias.\nRead :attr:`Usage.credits` and :attr:`Usage.costs` and do the division \u2014\nit is the same one this block does::\n\n remaining = u.credits.remaining // u.costs[capability]\n total = u.credits.total // u.costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical objects \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 because there is one balance behind all of\nthem, not a per-capability allowance.\n\nThese are **projections, not allowances**. Every billable capability draws\non the single balance in :attr:`Usage.credits`, at the weight in\n:attr:`Usage.costs`; this block answers \"how many ``/verify`` calls could I\nstill make if I spent everything on them\". Spending on any capability moves\nevery block, and the division floors \u2014 a ``verify`` block (weight 10) ticks\nonce per 10 credits spent anywhere.\n\n- ``quota_*`` \u2014 the monthly allowance projected into this unit; resets\n every period (see :attr:`Usage.quota_resets_at`). ``quota_used`` is\n derived as ``quota_total - quota_remaining``, so ``quota_used +\n quota_remaining == quota_total`` always holds.\n- ``bonus`` \u2014 :attr:`UsageCredits.extra`, the non-expiring part of the\n balance, in this unit. A user holding 5 extra credits sees\n ``assess.bonus == 5`` and ``verify.bonus == 0``: 5 credits does not\n buy a verification.\n\n``remaining`` is the usable capacity across both buckets.", + "properties": { + "bonus": { + "default": 0, + "title": "Bonus", + "type": "integer" + }, + "credits": { + "default": 0, + "deprecated": true, + "title": "Credits", + "type": "integer" + }, + "quota_remaining": { + "default": 0, + "title": "Quota Remaining", + "type": "integer" + }, + "quota_total": { + "default": 0, + "title": "Quota Total", + "type": "integer" + }, + "quota_used": { + "default": 0, + "title": "Quota Used", + "type": "integer" + }, + "remaining": { + "default": 0, + "title": "Remaining", + "type": "integer" + } + }, + "title": "UsageCapacity", + "type": "object" + }, + "UsageCredits": { + "additionalProperties": true, + "description": "The account's credit balance \u2014 the one pool every capability spends.\n\nEvery billable call debits this pool at the weight in :attr:`Usage.costs`\n(``/verify`` is 10 credits \u2014 5 at ``depth=\"low\"``, published as\n``cost_options[\"verify\"][\"depth\"][\"low\"]`` \u2014 ``/assess`` and ``/ask`` are 1, ``/extract`` is\nfree). Two buckets: the monthly allowance for the current plan, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` covers both\nand is what a call is checked against. ``bonus`` is the deprecated old\nname of ``extra``, the same number, removed on 2026-11-29.\n\nServers predating the credit pool (before 2026-08-29) don't send this block\nat all, and it then reads as all-zero \u2014 check ``usage.credits.total``\nbefore trusting the balance.", + "properties": { + "bonus": { + "default": 0, + "deprecated": true, + "title": "Bonus", + "type": "integer" + }, + "extra": { + "default": 0, + "title": "Extra", + "type": "integer" + }, + "remaining": { + "default": 0, + "title": "Remaining", + "type": "integer" + }, + "resets_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Resets At" + }, + "total": { + "default": 0, + "title": "Total", + "type": "integer" + }, + "used": { + "default": 0, + "title": "Used", + "type": "integer" + } + }, + "title": "UsageCredits", + "type": "object" + }, + "UsageExtract": { + "additionalProperties": true, + "description": "Daily ``/extract`` usage \u2014 a per-day rate limit, not credit-based.", + "properties": { + "calls_today": { + "default": 0, + "title": "Calls Today", + "type": "integer" + }, + "daily_limit": { + "default": 0, + "title": "Daily Limit", + "type": "integer" + }, + "unlimited": { + "default": false, + "title": "Unlimited", + "type": "boolean" + } + }, + "title": "UsageExtract", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Returned by ``GET /me/usage`` \u2014 the account's balance and what it buys.\n\n``credits`` is the balance and ``costs`` is the price list (credits per\ncall, keyed by capability). The ``verify`` / ``ask`` / ``assess`` blocks\nare **projections** of that one pool into each capability's unit \u2014 read\nwhichever is convenient, they all describe the same money, and spending on\none moves all of them.\n\n``extract`` is free at the pool (``costs[\"extract\"] == 0``) and is bounded\nby a per-account daily fair-use cap instead; it rejects with 429, never\n402.\n\n``costs`` names capabilities, one entry each, at the default price.\nPrices that depend on a request PARAMETER live in :attr:`cost_options`,\nnested capability \u2192 parameter \u2192 value. Divide ``credits.remaining`` by one\nof those yourself for the low-depth count \u2014 there is deliberately no\n``verify_low`` block beside ``verify``.", + "properties": { + "ask": { + "$ref": "#/$defs/UsageCapacity" + }, + "assess": { + "$ref": "#/$defs/UsageCapacity" + }, + "cost_options": { + "additionalProperties": { + "additionalProperties": { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + "type": "object" + }, + "title": "Cost Options", + "type": "object" + }, + "costs": { + "additionalProperties": { + "type": "integer" + }, + "title": "Costs", + "type": "object" + }, + "credits": { + "$ref": "#/$defs/UsageCredits" + }, + "extract": { + "$ref": "#/$defs/UsageExtract" + }, + "has_webhook_secret": { + "default": false, + "title": "Has Webhook Secret", + "type": "boolean" + }, + "plan": { + "default": "", + "title": "Plan", + "type": "string" + }, + "plan_label": { + "default": "", + "title": "Plan Label", + "type": "string" + }, + "quota_resets_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quota Resets At" + }, + "verify": { + "$ref": "#/$defs/UsageCapacity" + } + }, + "title": "Usage", + "type": "object" + }, + "Usage:validation": { + "$defs": { + "UsageCapacity": { + "additionalProperties": true, + "description": "DEPRECATED. One capability's share of the pool, in that capability's unit.\n\n**Removed 2026-11-29**, together with the per-block ``credits`` alias.\nRead :attr:`Usage.credits` and :attr:`Usage.costs` and do the division \u2014\nit is the same one this block does::\n\n remaining = u.credits.remaining // u.costs[capability]\n total = u.credits.total // u.costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical objects \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 because there is one balance behind all of\nthem, not a per-capability allowance.\n\nThese are **projections, not allowances**. Every billable capability draws\non the single balance in :attr:`Usage.credits`, at the weight in\n:attr:`Usage.costs`; this block answers \"how many ``/verify`` calls could I\nstill make if I spent everything on them\". Spending on any capability moves\nevery block, and the division floors \u2014 a ``verify`` block (weight 10) ticks\nonce per 10 credits spent anywhere.\n\n- ``quota_*`` \u2014 the monthly allowance projected into this unit; resets\n every period (see :attr:`Usage.quota_resets_at`). ``quota_used`` is\n derived as ``quota_total - quota_remaining``, so ``quota_used +\n quota_remaining == quota_total`` always holds.\n- ``bonus`` \u2014 :attr:`UsageCredits.extra`, the non-expiring part of the\n balance, in this unit. A user holding 5 extra credits sees\n ``assess.bonus == 5`` and ``verify.bonus == 0``: 5 credits does not\n buy a verification.\n\n``remaining`` is the usable capacity across both buckets.", + "properties": { + "bonus": { + "default": 0, + "title": "Bonus", + "type": "integer" + }, + "credits": { + "default": 0, + "deprecated": true, + "title": "Credits", + "type": "integer" + }, + "quota_remaining": { + "default": 0, + "title": "Quota Remaining", + "type": "integer" + }, + "quota_total": { + "default": 0, + "title": "Quota Total", + "type": "integer" + }, + "quota_used": { + "default": 0, + "title": "Quota Used", + "type": "integer" + }, + "remaining": { + "default": 0, + "title": "Remaining", + "type": "integer" + } + }, + "title": "UsageCapacity", + "type": "object" + }, + "UsageCredits": { + "additionalProperties": true, + "description": "The account's credit balance \u2014 the one pool every capability spends.\n\nEvery billable call debits this pool at the weight in :attr:`Usage.costs`\n(``/verify`` is 10 credits \u2014 5 at ``depth=\"low\"``, published as\n``cost_options[\"verify\"][\"depth\"][\"low\"]`` \u2014 ``/assess`` and ``/ask`` are 1, ``/extract`` is\nfree). Two buckets: the monthly allowance for the current plan, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` covers both\nand is what a call is checked against. ``bonus`` is the deprecated old\nname of ``extra``, the same number, removed on 2026-11-29.\n\nServers predating the credit pool (before 2026-08-29) don't send this block\nat all, and it then reads as all-zero \u2014 check ``usage.credits.total``\nbefore trusting the balance.", + "properties": { + "bonus": { + "default": 0, + "deprecated": true, + "title": "Bonus", + "type": "integer" + }, + "extra": { + "default": 0, + "title": "Extra", + "type": "integer" + }, + "remaining": { + "default": 0, + "title": "Remaining", + "type": "integer" + }, + "resets_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Resets At" + }, + "total": { + "default": 0, + "title": "Total", + "type": "integer" + }, + "used": { + "default": 0, + "title": "Used", + "type": "integer" + } + }, + "title": "UsageCredits", + "type": "object" + }, + "UsageExtract": { + "additionalProperties": true, + "description": "Daily ``/extract`` usage \u2014 a per-day rate limit, not credit-based.", + "properties": { + "calls_today": { + "default": 0, + "title": "Calls Today", + "type": "integer" + }, + "daily_limit": { + "default": 0, + "title": "Daily Limit", + "type": "integer" + }, + "unlimited": { + "default": false, + "title": "Unlimited", + "type": "boolean" + } + }, + "title": "UsageExtract", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Returned by ``GET /me/usage`` \u2014 the account's balance and what it buys.\n\n``credits`` is the balance and ``costs`` is the price list (credits per\ncall, keyed by capability). The ``verify`` / ``ask`` / ``assess`` blocks\nare **projections** of that one pool into each capability's unit \u2014 read\nwhichever is convenient, they all describe the same money, and spending on\none moves all of them.\n\n``extract`` is free at the pool (``costs[\"extract\"] == 0``) and is bounded\nby a per-account daily fair-use cap instead; it rejects with 429, never\n402.\n\n``costs`` names capabilities, one entry each, at the default price.\nPrices that depend on a request PARAMETER live in :attr:`cost_options`,\nnested capability \u2192 parameter \u2192 value. Divide ``credits.remaining`` by one\nof those yourself for the low-depth count \u2014 there is deliberately no\n``verify_low`` block beside ``verify``.", + "properties": { + "ask": { + "$ref": "#/$defs/UsageCapacity" + }, + "assess": { + "$ref": "#/$defs/UsageCapacity" + }, + "cost_options": { + "additionalProperties": { + "additionalProperties": { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + "type": "object" + }, + "title": "Cost Options", + "type": "object" + }, + "costs": { + "additionalProperties": { + "type": "integer" + }, + "title": "Costs", + "type": "object" + }, + "credits": { + "$ref": "#/$defs/UsageCredits" + }, + "extract": { + "$ref": "#/$defs/UsageExtract" + }, + "has_webhook_secret": { + "default": false, + "title": "Has Webhook Secret", + "type": "boolean" + }, + "plan": { + "default": "", + "title": "Plan", + "type": "string" + }, + "plan_label": { + "default": "", + "title": "Plan Label", + "type": "string" + }, + "quota_resets_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Quota Resets At" + }, + "verify": { + "$ref": "#/$defs/UsageCapacity" + } + }, + "title": "Usage", + "type": "object" + }, + "UsageCapacity:serialization": { + "additionalProperties": true, + "description": "DEPRECATED. One capability's share of the pool, in that capability's unit.\n\n**Removed 2026-11-29**, together with the per-block ``credits`` alias.\nRead :attr:`Usage.credits` and :attr:`Usage.costs` and do the division \u2014\nit is the same one this block does::\n\n remaining = u.credits.remaining // u.costs[capability]\n total = u.credits.total // u.costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical objects \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 because there is one balance behind all of\nthem, not a per-capability allowance.\n\nThese are **projections, not allowances**. Every billable capability draws\non the single balance in :attr:`Usage.credits`, at the weight in\n:attr:`Usage.costs`; this block answers \"how many ``/verify`` calls could I\nstill make if I spent everything on them\". Spending on any capability moves\nevery block, and the division floors \u2014 a ``verify`` block (weight 10) ticks\nonce per 10 credits spent anywhere.\n\n- ``quota_*`` \u2014 the monthly allowance projected into this unit; resets\n every period (see :attr:`Usage.quota_resets_at`). ``quota_used`` is\n derived as ``quota_total - quota_remaining``, so ``quota_used +\n quota_remaining == quota_total`` always holds.\n- ``bonus`` \u2014 :attr:`UsageCredits.extra`, the non-expiring part of the\n balance, in this unit. A user holding 5 extra credits sees\n ``assess.bonus == 5`` and ``verify.bonus == 0``: 5 credits does not\n buy a verification.\n\n``remaining`` is the usable capacity across both buckets.", + "properties": { + "bonus": { + "default": 0, + "title": "Bonus", + "type": "integer" + }, + "credits": { + "default": 0, + "deprecated": true, + "title": "Credits", + "type": "integer" + }, + "quota_remaining": { + "default": 0, + "title": "Quota Remaining", + "type": "integer" + }, + "quota_total": { + "default": 0, + "title": "Quota Total", + "type": "integer" + }, + "quota_used": { + "default": 0, + "title": "Quota Used", + "type": "integer" + }, + "remaining": { + "default": 0, + "title": "Remaining", + "type": "integer" + } + }, + "title": "UsageCapacity", + "type": "object" + }, + "UsageCapacity:validation": { + "additionalProperties": true, + "description": "DEPRECATED. One capability's share of the pool, in that capability's unit.\n\n**Removed 2026-11-29**, together with the per-block ``credits`` alias.\nRead :attr:`Usage.credits` and :attr:`Usage.costs` and do the division \u2014\nit is the same one this block does::\n\n remaining = u.credits.remaining // u.costs[capability]\n total = u.credits.total // u.costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical objects \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 because there is one balance behind all of\nthem, not a per-capability allowance.\n\nThese are **projections, not allowances**. Every billable capability draws\non the single balance in :attr:`Usage.credits`, at the weight in\n:attr:`Usage.costs`; this block answers \"how many ``/verify`` calls could I\nstill make if I spent everything on them\". Spending on any capability moves\nevery block, and the division floors \u2014 a ``verify`` block (weight 10) ticks\nonce per 10 credits spent anywhere.\n\n- ``quota_*`` \u2014 the monthly allowance projected into this unit; resets\n every period (see :attr:`Usage.quota_resets_at`). ``quota_used`` is\n derived as ``quota_total - quota_remaining``, so ``quota_used +\n quota_remaining == quota_total`` always holds.\n- ``bonus`` \u2014 :attr:`UsageCredits.extra`, the non-expiring part of the\n balance, in this unit. A user holding 5 extra credits sees\n ``assess.bonus == 5`` and ``verify.bonus == 0``: 5 credits does not\n buy a verification.\n\n``remaining`` is the usable capacity across both buckets.", + "properties": { + "bonus": { + "default": 0, + "title": "Bonus", + "type": "integer" + }, + "credits": { + "default": 0, + "deprecated": true, + "title": "Credits", + "type": "integer" + }, + "quota_remaining": { + "default": 0, + "title": "Quota Remaining", + "type": "integer" + }, + "quota_total": { + "default": 0, + "title": "Quota Total", + "type": "integer" + }, + "quota_used": { + "default": 0, + "title": "Quota Used", + "type": "integer" + }, + "remaining": { + "default": 0, + "title": "Remaining", + "type": "integer" + } + }, + "title": "UsageCapacity", + "type": "object" + }, + "UsageCredits:serialization": { + "additionalProperties": true, + "description": "The account's credit balance \u2014 the one pool every capability spends.\n\nEvery billable call debits this pool at the weight in :attr:`Usage.costs`\n(``/verify`` is 10 credits \u2014 5 at ``depth=\"low\"``, published as\n``cost_options[\"verify\"][\"depth\"][\"low\"]`` \u2014 ``/assess`` and ``/ask`` are 1, ``/extract`` is\nfree). Two buckets: the monthly allowance for the current plan, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` covers both\nand is what a call is checked against. ``bonus`` is the deprecated old\nname of ``extra``, the same number, removed on 2026-11-29.\n\nServers predating the credit pool (before 2026-08-29) don't send this block\nat all, and it then reads as all-zero \u2014 check ``usage.credits.total``\nbefore trusting the balance.", + "properties": { + "bonus": { + "default": 0, + "deprecated": true, + "title": "Bonus", + "type": "integer" + }, + "extra": { + "default": 0, + "title": "Extra", + "type": "integer" + }, + "remaining": { + "default": 0, + "title": "Remaining", + "type": "integer" + }, + "resets_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Resets At" + }, + "total": { + "default": 0, + "title": "Total", + "type": "integer" + }, + "used": { + "default": 0, + "title": "Used", + "type": "integer" + } + }, + "title": "UsageCredits", + "type": "object" + }, + "UsageCredits:validation": { + "additionalProperties": true, + "description": "The account's credit balance \u2014 the one pool every capability spends.\n\nEvery billable call debits this pool at the weight in :attr:`Usage.costs`\n(``/verify`` is 10 credits \u2014 5 at ``depth=\"low\"``, published as\n``cost_options[\"verify\"][\"depth\"][\"low\"]`` \u2014 ``/assess`` and ``/ask`` are 1, ``/extract`` is\nfree). Two buckets: the monthly allowance for the current plan, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` covers both\nand is what a call is checked against. ``bonus`` is the deprecated old\nname of ``extra``, the same number, removed on 2026-11-29.\n\nServers predating the credit pool (before 2026-08-29) don't send this block\nat all, and it then reads as all-zero \u2014 check ``usage.credits.total``\nbefore trusting the balance.", + "properties": { + "bonus": { + "default": 0, + "deprecated": true, + "title": "Bonus", + "type": "integer" + }, + "extra": { + "default": 0, + "title": "Extra", + "type": "integer" + }, + "remaining": { + "default": 0, + "title": "Remaining", + "type": "integer" + }, + "resets_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Resets At" + }, + "total": { + "default": 0, + "title": "Total", + "type": "integer" + }, + "used": { + "default": 0, + "title": "Used", + "type": "integer" + } + }, + "title": "UsageCredits", + "type": "object" + }, + "UsageExtract:serialization": { + "additionalProperties": true, + "description": "Daily ``/extract`` usage \u2014 a per-day rate limit, not credit-based.", + "properties": { + "calls_today": { + "default": 0, + "title": "Calls Today", + "type": "integer" + }, + "daily_limit": { + "default": 0, + "title": "Daily Limit", + "type": "integer" + }, + "unlimited": { + "default": false, + "title": "Unlimited", + "type": "boolean" + } + }, + "title": "UsageExtract", + "type": "object" + }, + "UsageExtract:validation": { + "additionalProperties": true, + "description": "Daily ``/extract`` usage \u2014 a per-day rate limit, not credit-based.", + "properties": { + "calls_today": { + "default": 0, + "title": "Calls Today", + "type": "integer" + }, + "daily_limit": { + "default": 0, + "title": "Daily Limit", + "type": "integer" + }, + "unlimited": { + "default": false, + "title": "Unlimited", + "type": "boolean" + } + }, + "title": "UsageExtract", + "type": "object" + }, + "Verification:serialization": { + "$defs": { + "Assessment": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "Audit": { + "additionalProperties": true, + "description": "Nested explainability block \u2014 for callers who want the panel's work.", + "properties": { + "adjudication_summary": { + "default": "", + "title": "Adjudication Summary", + "type": "string" + }, + "assessments": { + "items": { + "$ref": "#/$defs/Assessment" + }, + "title": "Assessments", + "type": "array" + }, + "debate_con": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "debate_pro": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "panel_agreement": { + "default": "", + "title": "Panel Agreement", + "type": "string" + } + }, + "title": "Audit", + "type": "object" + }, + "Coverage": { + "additionalProperties": true, + "description": "Whether this verdict carries Lenz's warranty, for YOUR account.\n\nPresent only when covered verification is enabled on the account's plan;\n``None`` otherwise. It describes the pair (your account, this analysis),\nnot the analysis alone \u2014 two accounts can hold two certificates over one\ncached verdict, so the block you see is never another customer's.\n\n``reasons`` is empty exactly when ``status`` is not ``\"uncovered\"``.\n\nThe money fields are three, not two: ``currency`` is ISO 4217 and the\namounts are integers in **major units** \u2014 ``cap=10000`` means ten\nthousand, not a hundred. They are contract figures, not amounts a payment\nprocessor charges. Read ``currency``; do not assume EUR.", + "properties": { + "aggregate": { + "default": 0, + "title": "Aggregate", + "type": "integer" + }, + "as_of": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "As Of" + }, + "cap": { + "default": 0, + "title": "Cap", + "type": "integer" + }, + "certificate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Id" + }, + "certificate_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Url" + }, + "currency": { + "default": "", + "title": "Currency", + "type": "string" + }, + "reasons": { + "items": { + "type": "string" + }, + "title": "Reasons", + "type": "array" + }, + "status": { + "default": "uncovered", + "title": "Status", + "type": "string" + }, + "terms_version": { + "default": "", + "title": "Terms Version", + "type": "string" + } + }, + "title": "Coverage", + "type": "object" + }, + "DebateSide": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + }, + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "Source": { + "additionalProperties": true, + "description": "A single citation backing a verification.", + "properties": { + "date": { + "default": "", + "title": "Date", + "type": "string" + }, + "snippet": { + "default": "", + "title": "Snippet", + "type": "string" + }, + "source_name": { + "default": "", + "title": "Source Name", + "type": "string" + }, + "title": { + "default": "", + "title": "Title", + "type": "string" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + } + }, + "title": "Source", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Full verification report \u2014 returned by ``verify_and_wait``,\n``verifications.get``, the ``/verify/status/{task_id}`` polling\nendpoint, and the webhook payload.\n\nThe verdict block is FLAT at top level (was nested ``Verdict`` object\npre-unify). ``created_at`` + ``modified_at`` are the only timestamp\nfields on the API surface \u2014 editorial ``published_at`` is internal-only.\n\n1.1.0: dropped ``url`` and ``visibility``. API claims are private by\ndefault and referenced by ``verification_id`` only. Cache-hit on\nanother customer's claim is transparent \u2014 the customer always sees\ntheir own ``verification_id``, never another customer's.\n\nLater: ``visibility`` returns \u2014 'private' | 'unlisted' | 'public'. It\nechoes what you set on submit ('private'/'unlisted' are settable;\n'public' can only be read, for listed claims). ``url`` stays dropped.", + "properties": { + "audit": { + "$ref": "#/$defs/Audit" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "coverage": { + "anyOf": [ + { + "$ref": "#/$defs/Coverage" + }, + { + "type": "null" + } + ], + "default": null + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "default": "", + "title": "Depth", + "type": "string" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "presumed_intent": { + "default": "", + "title": "Presumed Intent", + "type": "string" + }, + "sources": { + "items": { + "$ref": "#/$defs/Source" + }, + "title": "Sources", + "type": "array" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + }, + "visibility": { + "default": "", + "title": "Visibility", + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Verification", + "type": "object" + }, + "Verification:validation": { + "$defs": { + "Assessment": { + "additionalProperties": true, + "description": "One reviewer's structured assessment.\n\nCurrent verifications carry three reviewers, ``Reviewer A`` to\n``Reviewer C``, all running the same checks over the evidence, plus\n``Reviewer D`` and ``Reviewer E`` when those three disagree. Their\n``focus_area`` reads ``\"Sources, evidence fit and wording\"``, and\n``warnings`` holds the source issues, evidence gaps and precision issues\nthat reviewer found, in one list.\n\nOlder verifications carry specialist panelists instead, one warning\ncategory each: logical fallacies (Logic Examiner), precision issues\n(Precision Analyst), weakest sources (Source Auditor) and, before\n2026-06, missing context (Context Analyst).\n\n``panelist_name`` is a display value, not a stable key \u2014 don't branch\non it. ``score`` is a reviewer-level 1-10 sub-score, distinct from the\ntop-level ``lenz_score`` on a ``Verification``.", + "properties": { + "focus_area": { + "default": "", + "title": "Focus Area", + "type": "string" + }, + "panelist_name": { + "default": "", + "title": "Panelist Name", + "type": "string" + }, + "reasoning": { + "default": "", + "title": "Reasoning", + "type": "string" + }, + "score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Score" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Assessment", + "type": "object" + }, + "Audit": { + "additionalProperties": true, + "description": "Nested explainability block \u2014 for callers who want the panel's work.", + "properties": { + "adjudication_summary": { + "default": "", + "title": "Adjudication Summary", + "type": "string" + }, + "assessments": { + "items": { + "$ref": "#/$defs/Assessment" + }, + "title": "Assessments", + "type": "array" + }, + "debate_con": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "debate_pro": { + "anyOf": [ + { + "$ref": "#/$defs/DebateSide" + }, + { + "type": "null" + } + ], + "default": null + }, + "panel_agreement": { + "default": "", + "title": "Panel Agreement", + "type": "string" + } + }, + "title": "Audit", + "type": "object" + }, + "Coverage": { + "additionalProperties": true, + "description": "Whether this verdict carries Lenz's warranty, for YOUR account.\n\nPresent only when covered verification is enabled on the account's plan;\n``None`` otherwise. It describes the pair (your account, this analysis),\nnot the analysis alone \u2014 two accounts can hold two certificates over one\ncached verdict, so the block you see is never another customer's.\n\n``reasons`` is empty exactly when ``status`` is not ``\"uncovered\"``.\n\nThe money fields are three, not two: ``currency`` is ISO 4217 and the\namounts are integers in **major units** \u2014 ``cap=10000`` means ten\nthousand, not a hundred. They are contract figures, not amounts a payment\nprocessor charges. Read ``currency``; do not assume EUR.", + "properties": { + "aggregate": { + "default": 0, + "title": "Aggregate", + "type": "integer" + }, + "as_of": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "As Of" + }, + "cap": { + "default": 0, + "title": "Cap", + "type": "integer" + }, + "certificate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Id" + }, + "certificate_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Certificate Url" + }, + "currency": { + "default": "", + "title": "Currency", + "type": "string" + }, + "reasons": { + "items": { + "type": "string" + }, + "title": "Reasons", + "type": "array" + }, + "status": { + "default": "uncovered", + "title": "Status", + "type": "string" + }, + "terms_version": { + "default": "", + "title": "Terms Version", + "type": "string" + } + }, + "title": "Coverage", + "type": "object" + }, + "DebateSide": { + "additionalProperties": true, + "description": "One side of the adversarial debate transcript.", + "properties": { + "argument": { + "default": "", + "title": "Argument", + "type": "string" + }, + "rebuttal": { + "default": "", + "title": "Rebuttal", + "type": "string" + }, + "role": { + "default": "", + "title": "Role", + "type": "string" + } + }, + "title": "DebateSide", + "type": "object" + }, + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "Source": { + "additionalProperties": true, + "description": "A single citation backing a verification.", + "properties": { + "date": { + "default": "", + "title": "Date", + "type": "string" + }, + "snippet": { + "default": "", + "title": "Snippet", + "type": "string" + }, + "source_name": { + "default": "", + "title": "Source Name", + "type": "string" + }, + "title": { + "default": "", + "title": "Title", + "type": "string" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + } + }, + "title": "Source", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Full verification report \u2014 returned by ``verify_and_wait``,\n``verifications.get``, the ``/verify/status/{task_id}`` polling\nendpoint, and the webhook payload.\n\nThe verdict block is FLAT at top level (was nested ``Verdict`` object\npre-unify). ``created_at`` + ``modified_at`` are the only timestamp\nfields on the API surface \u2014 editorial ``published_at`` is internal-only.\n\n1.1.0: dropped ``url`` and ``visibility``. API claims are private by\ndefault and referenced by ``verification_id`` only. Cache-hit on\nanother customer's claim is transparent \u2014 the customer always sees\ntheir own ``verification_id``, never another customer's.\n\nLater: ``visibility`` returns \u2014 'private' | 'unlisted' | 'public'. It\nechoes what you set on submit ('private'/'unlisted' are settable;\n'public' can only be read, for listed claims). ``url`` stays dropped.", + "properties": { + "audit": { + "$ref": "#/$defs/Audit" + }, + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "coverage": { + "anyOf": [ + { + "$ref": "#/$defs/Coverage" + }, + { + "type": "null" + } + ], + "default": null + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "depth": { + "default": "", + "title": "Depth", + "type": "string" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "presumed_intent": { + "default": "", + "title": "Presumed Intent", + "type": "string" + }, + "sources": { + "items": { + "$ref": "#/$defs/Source" + }, + "title": "Sources", + "type": "array" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + }, + "visibility": { + "default": "", + "title": "Visibility", + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "Verification", + "type": "object" + }, + "VerificationList:serialization": { + "$defs": { + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "VerificationListItem": { + "additionalProperties": true, + "description": "Compact item for the verifications list endpoint and the public\nlibrary list. Slim shape \u2014 no ``url`` (reference by\n``verification_id``), no ``visibility`` (1.1.0).", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "VerificationListItem", + "type": "object" + } + }, + "additionalProperties": true, + "properties": { + "items": { + "items": { + "$ref": "#/$defs/VerificationListItem" + }, + "title": "Items", + "type": "array" + }, + "page": { + "default": 1, + "title": "Page", + "type": "integer" + }, + "page_size": { + "default": 20, + "title": "Page Size", + "type": "integer" + }, + "total": { + "default": 0, + "title": "Total", + "type": "integer" + } + }, + "title": "VerificationList", + "type": "object" + }, + "VerificationList:validation": { + "$defs": { + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + }, + "VerificationListItem": { + "additionalProperties": true, + "description": "Compact item for the verifications list endpoint and the public\nlibrary list. Slim shape \u2014 no ``url`` (reference by\n``verification_id``), no ``visibility`` (1.1.0).", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "VerificationListItem", + "type": "object" + } + }, + "additionalProperties": true, + "properties": { + "items": { + "items": { + "$ref": "#/$defs/VerificationListItem" + }, + "title": "Items", + "type": "array" + }, + "page": { + "default": 1, + "title": "Page", + "type": "integer" + }, + "page_size": { + "default": 20, + "title": "Page Size", + "type": "integer" + }, + "total": { + "default": 0, + "title": "Total", + "type": "integer" + } + }, + "title": "VerificationList", + "type": "object" + }, + "VerificationListItem:serialization": { + "$defs": { + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Compact item for the verifications list endpoint and the public\nlibrary list. Slim shape \u2014 no ``url`` (reference by\n``verification_id``), no ``visibility`` (1.1.0).", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "VerificationListItem", + "type": "object" + }, + "VerificationListItem:validation": { + "$defs": { + "EntityRef": { + "additionalProperties": true, + "description": "An entity referenced in the claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42``) when the entity\nwas resolved against Lenz's internal catalog; ``None`` otherwise.", + "properties": { + "name": { + "default": "", + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Qid" + } + }, + "title": "EntityRef", + "type": "object" + } + }, + "additionalProperties": true, + "description": "Compact item for the verifications list endpoint and the public\nlibrary list. Slim shape \u2014 no ``url`` (reference by\n``verification_id``), no ``visibility`` (1.1.0).", + "properties": { + "claim": { + "default": "", + "title": "Claim", + "type": "string" + }, + "confidence": { + "default": "low", + "title": "Confidence", + "type": "string" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Created At" + }, + "domain": { + "default": "", + "title": "Domain", + "type": "string" + }, + "entities": { + "items": { + "$ref": "#/$defs/EntityRef" + }, + "title": "Entities", + "type": "array" + }, + "executive_summary": { + "default": "", + "title": "Executive Summary", + "type": "string" + }, + "key_finding": { + "default": "", + "title": "Key Finding", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "lenz_score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lenz Score" + }, + "modified_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Modified At" + }, + "suggested_rewrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Rewrite" + }, + "verdict": { + "default": "", + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "default": "", + "title": "Verification Id", + "type": "string" + } + }, + "title": "VerificationListItem", + "type": "object" + } +} diff --git a/tests/parity_generate.py b/tests/parity_generate.py index e104cb9..cf386f2 100644 --- a/tests/parity_generate.py +++ b/tests/parity_generate.py @@ -15,21 +15,29 @@ import json import sys +from pathlib import Path from parity_observe import FIXTURES, load, names, observe from parity_requests import bodies +from parity_static import pickles, schemas import lenz_io def main() -> None: print(f"observing with lenz_io from {lenz_io.__file__}", file=sys.stderr) + if "--allow-current" not in sys.argv and Path(lenz_io.__file__).resolve().is_relative_to( + Path(__file__).resolve().parents[1] + ): + raise SystemExit("refusing: this is the current code; put the previous release first on PYTHONPATH") out = FIXTURES / "expected" out.mkdir(exist_ok=True) for name in names(): result = observe(name, load("legacy", name)) (out / name).write_text(json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True) + "\n") (FIXTURES / "requests.json").write_text(json.dumps(bodies(), indent=2, sort_keys=True) + "\n") + (FIXTURES / "schemas.json").write_text(json.dumps(schemas(), indent=2, sort_keys=True) + "\n") + (FIXTURES / "pickles.json").write_text(json.dumps(pickles(), indent=2, sort_keys=True) + "\n") print(f"wrote {len(names())} files and requests.json", file=sys.stderr) diff --git a/tests/parity_observe.py b/tests/parity_observe.py index 242d5e0..0ed28d7 100644 --- a/tests/parity_observe.py +++ b/tests/parity_observe.py @@ -96,9 +96,9 @@ def attr_view(obj: Any, template: Any) -> Any: out[key] = MISSING if value is MISSING else attr_view(value, sub) return out if isinstance(template, list): - items = list(obj) if isinstance(obj, (list, tuple)) else [] - if len(items) != len(template): + if not isinstance(obj, (list, tuple)) or len(obj) != len(template): return _jsonable(obj) + items = list(obj) return [attr_view(o, t) for o, t in zip(items, template, strict=True)] return _jsonable(obj) @@ -186,8 +186,10 @@ def observe(name: str, fixture: dict[str, Any]) -> dict[str, Any]: cls = _model_for(name) if cls is None: return result - model = cls.model_validate(body) + model = _extract_via_client(name, body) if cls is models.ExtractedClaims else cls.model_validate(body) result["dump"] = model.model_dump(mode="json") + result["dump_unset"] = model.model_dump(mode="json", exclude_unset=True) + result["repr"] = repr(model) result["render_json"] = None if cls is models.ExtractedClaims: result["render"] = _render(render_mod.render_extract, model) @@ -225,6 +227,26 @@ def observe(name: str, fixture: dict[str, Any]) -> dict[str, Any]: return result +def located(name: str) -> bool: + """Whether the recorded /extract call asked to locate its claims.""" + return "locate" in name + + +def _extract_via_client(name: str, body: dict[str, Any]) -> Any: + """``client.extract`` on the recorded body, with the recorded ``locate``.""" + import respx + + from lenz_io.client import DEFAULT_BASE_URL + + client = Lenz(api_key="lenz_" + "0" * 32) + try: + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.post("/extract").respond(200, json=body) + return client.extract(text="text", locate=True if located(name) else None) + finally: + client.close() + + def load(shape: str, name: str) -> dict[str, Any]: return json.loads((FIXTURES / shape / name).read_text()) diff --git a/tests/parity_static.py b/tests/parity_static.py new file mode 100644 index 0000000..8883f0d --- /dev/null +++ b/tests/parity_static.py @@ -0,0 +1,46 @@ +"""Model schemas and pickles, frozen by ``parity_generate.py`` from the +previous release (``schemas.json``, ``pickles.json``) and checked by +``test_parity.py``: this release must publish the same JSON schemas for every +model, and unpickle and dump what the previous release pickled.""" + +from __future__ import annotations + +import base64 +import pickle +import warnings +from typing import Any + +from parity_observe import _model_for, load, names +from pydantic import BaseModel + +from lenz_io import models + + +def model_classes() -> dict[str, type[BaseModel]]: + return { + name: obj + for name in models.__all__ + if isinstance(obj := getattr(models, name), type) and issubclass(obj, BaseModel) + } + + +def schemas() -> dict[str, Any]: + out = {} + for name, cls in model_classes().items(): + for mode in ("validation", "serialization"): + out[f"{name}:{mode}"] = cls.model_json_schema(mode=mode) + return out + + +def _pickle_names() -> list[str]: + return [n for n in names() if _model_for(n) is not None and load("legacy", n)["status"] < 400] + + +def pickles() -> dict[str, str]: + out = {} + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + for name in _pickle_names(): + model = _model_for(name).model_validate(load("legacy", name)["body"]) + out[name] = base64.b64encode(pickle.dumps(model, protocol=4)).decode() + return out diff --git a/tests/test_parity.py b/tests/test_parity.py index 90a942e..ee0b578 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -26,7 +26,10 @@ from typing import Any import pytest -from parity_observe import FIXTURES, MISSING, _model_for, attr_view, load, names, observe +from parity_observe import FIXTURES, MISSING, _extract_via_client, _model_for, attr_view, load, names, observe + +from lenz_io import models +from lenz_io.webhooks import parse_webhook EXPECTED = FIXTURES / "expected" @@ -38,8 +41,8 @@ # the server's sentences (error / failure text), the codes and field errors it # sends on a 422, `chain_id`, the review / citation-check webhook `task_id`, # and a row hint the newer shape does not carry. The single entries in -# ``KNOWN_GAPS`` below fall in those categories too, plus two where the two -# recordings simply hold different ids. +# ``KNOWN_GAPS`` below fall in those categories too, plus one where the two +# recordings number an id differently. #: An exception's text fields: built from the body's ``detail``. _MESSAGE_PATHS = ("message", "cause", "friendly_text", "payload_json.error.message") @@ -101,23 +104,9 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, ("extract__not_a_claim_beside_claims.json", "dump.status"): ( "the newer shape answers `ready` when claims came back beside a non-claim" ), - ("verify__list_200.json", "dump.items[1].verification_id"): "the two recordings list different verifications", - ("verify__list_200.json", "dump.items[1].created_at"): "the two recordings list different verifications", - ( - "verify__verification_200_covered.json", - "dump.coverage.certificate_id", - ): "the two recordings issued different certificates", - ( - "verify__verification_200_covered.json", - "dump.coverage.certificate_url", - ): "the two recordings issued different certificates", - ("review__get_completed_issues_all_verified.json", "dump.claims[1].verification.task_id"): ( - "the two recordings ran different tasks" + ("webhook__review_completed.json", "event.review.claims[0].verification.task_id"): ( + "the two recordings number their task ids differently (the newer payload has no delivery task_id)" ), - ( - "review__get_completed_issues_full.json", - "dump.claims[2].verification.task_id", - ): "the two recordings ran different tasks", } @@ -162,11 +151,23 @@ def test_original_shape_is_unchanged(name: str) -> None: def _canonical_view(name: str, expected: dict[str, Any]) -> dict[str, Any]: fixture = load("canonical", name) got = _old_event_attrs(observe(name, fixture), expected) - view = {k: v for k, v in got.items() if k not in ("dump", "render_json")} + view = {k: v for k, v in got.items() if k not in ("dump", "dump_unset", "repr", "render_json")} + if "event" in expected: + # A review or citation check on an event: read it by attribute too. + event = parse_webhook(fixture["body"]) + for key, old in expected["event"].items(): + value = getattr(event, key, None) + if isinstance(old, dict) and hasattr(value, "model_dump"): + view["event"][key] = attr_view(value, old) if "dump" in expected: with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) - model = _model_for(name).model_validate(fixture["body"]) + cls = _model_for(name) + model = ( + _extract_via_client(name, fixture["body"]) + if cls is models.ExtractedClaims + else cls.model_validate(fixture["body"]) + ) view["dump"] = attr_view(model, expected["dump"]) returned = expected.get("wait", {}).get("returned") if returned is not None: @@ -181,7 +182,7 @@ def _canonical_names() -> list[str]: @pytest.mark.parametrize("name", _canonical_names()) def test_newer_shape_reads_the_same(name: str) -> None: - expected = {k: v for k, v in _expected(name).items() if k != "render_json"} + expected = {k: v for k, v in _expected(name).items() if k not in ("render_json", "dump_unset", "repr")} got = _canonical_view(name, expected) legacy, canonical = load("legacy", name), load("canonical", name) gaps = [ @@ -194,7 +195,7 @@ def test_every_known_gap_is_still_a_gap() -> None: """A listed gap that closed is stale: drop it, so the list stays exact.""" stale = [] for (name, path), _reason in KNOWN_GAPS.items(): - expected = {k: v for k, v in _expected(name).items() if k != "render_json"} + expected = {k: v for k, v in _expected(name).items() if k not in ("render_json", "dump_unset", "repr")} if path not in {p for p, _, _ in _diff(expected, _canonical_view(name, expected))}: stale.append((name, path)) assert not stale @@ -211,3 +212,35 @@ def test_request_bodies_match_the_previous_release() -> None: from parity_requests import bodies assert bodies() == json.loads((FIXTURES / "requests.json").read_text()) + + +# ── Schemas and pickles ───────────────────────────────────────────────────── + + +def test_model_schemas_match_the_previous_release() -> None: + """Every model the previous release had publishes the same validation and + serialization JSON schema: the newer names are properties, not fields.""" + from parity_static import schemas + + def _shape(schema: Any) -> Any: + # Doc text (class docstrings) may change; the shape may not. + if isinstance(schema, dict): + return {k: _shape(v) for k, v in schema.items() if k != "description"} + if isinstance(schema, list): + return [_shape(v) for v in schema] + return schema + + frozen = _shape(json.loads((FIXTURES / "schemas.json").read_text())) + current = _shape(schemas()) + assert {k: current.get(k) for k in frozen} == frozen + + +def test_objects_pickled_by_the_previous_release_still_dump() -> None: + import base64 + import pickle + + frozen = json.loads((FIXTURES / "pickles.json").read_text()) + assert frozen + for name, blob in frozen.items(): + model = pickle.loads(base64.b64decode(blob)) + assert model.model_dump(mode="json") == _expected(name)["dump"], name diff --git a/tests/test_read_both_shapes.py b/tests/test_read_both_shapes.py index 83eee5c..7212622 100644 --- a/tests/test_read_both_shapes.py +++ b/tests/test_read_both_shapes.py @@ -236,7 +236,9 @@ def test_usage_blocks_are_computed_from_the_pool(name): ) def test_review_new_names_from_both_shapes(name): legacy, canonical = (ReviewFull.model_validate(b) for b in _both(name)) - assert legacy.summary.claim_limit_exceeded == canonical.summary.claim_limit_exceeded + # The original shape says only that the claim limit was REACHED. + assert legacy.summary.claim_limit_exceeded is None + assert canonical.summary.claim_limit_exceeded == bool(canonical.more_claims) assert legacy.summary.citation_limit_exceeded == canonical.summary.citation_limit_exceeded for a, b in zip(legacy.claims, canonical.claims, strict=True): assert a.assessment.more_claims == b.assessment.more_claims @@ -251,14 +253,47 @@ def test_review_failure_code_reads_the_new_spelling(): assert FailureBlock.model_validate({"failure_reason": "not_a_claim"}).code == "no_checkable_claim" -# ── dumps show what was sent ── +# ── the original shape is parsed exactly as before ── -def test_model_dump_shows_the_newer_shape_as_sent(): +def test_newer_shape_dump_keeps_every_key_sent(): canonical = load("canonical", "assess__list_mixed_rows.json")["body"] - assert AssessResponse.model_validate(canonical).model_dump(mode="json") == canonical - canonical = load("canonical", "verify__batch_202.json")["body"] - assert BatchAccepted.model_validate(canonical).model_dump(mode="json") == canonical + dumped = AssessResponse.model_validate(canonical).model_dump(mode="json") + assert set(canonical) <= set(dumped) + assert all(set(row) <= set(d) for row, d in zip(canonical["claims"], dumped["claims"], strict=True)) + + +def test_explicit_null_retryable_is_kept(): + st = TaskStatus.model_validate({"status": "failed", "retryable": None, "failure": {"code": "x", "retryable": True}}) + assert st.retryable is None + assert st.failure is not None and st.failure.retryable is True + + +def test_sparse_usage_dumps_every_default(): + assert set(Usage.model_validate({"plan": "free"}).model_dump()) == set(Usage.model_fields) + + +def test_original_shape_keeps_unset_fields_unset(): + body = {"claim": "x", "verdict": "True", "confidence": "high"} + assert AssessClaim.model_validate(body).model_dump(exclude_unset=True) == body + assert AssessResponse.model_validate({"claims": []}).model_dump(exclude_unset=True) == {"claims": []} + + +def test_new_names_are_properties_not_fields(): + for cls, name in ( + (AssessClaim, "status"), + (AssessClaim, "failure"), + (ExtractedClaims, "claims"), + (TaskStatus, "failure"), + (Verification, "completed_at"), + (FailureBlock, "code"), + ): + assert name not in cls.model_fields + + +def test_model_copy_update_shows_in_the_dump(): + row = AssessClaim.model_validate({"claim": "x", "verdict": "True"}) + assert row.model_copy(update={"verdict": "False"}).model_dump()["verdict"] == "False" # ── webhooks ── @@ -296,6 +331,26 @@ def test_completed_webhook_result_from_both_shapes(): assert legacy.result["modified_at"] == canonical.result["modified_at"] +def test_needs_input_with_a_null_domain_does_not_raise(): + event = parse_webhook( + { + "event": "verification.needs_input", + "task_id": "t", + "needs_input": {"claims": [{"text": "A", "domain": None}]}, + } + ) + assert event.needs_input == {"claims": [{"text": "A", "domain": None}]} + assert [c.claim for c in event.claims] == ["A"] + + +def test_legacy_webhook_event_still_serialises(): + import dataclasses + + event = parse_webhook(load("legacy", "webhook__verification_failed_not_a_claim.json")["body"]) + assert json.loads(json.dumps(dataclasses.asdict(event)))["error"] == "not_a_claim" + assert "failure" not in dataclasses.asdict(event) + + @pytest.mark.parametrize("name", ["webhook__review_completed.json", "webhook__citecheck_completed.json"]) def test_review_webhook_without_task_id(name): canonical = parse_webhook(load("canonical", name)["body"]) @@ -336,10 +391,20 @@ def test_rate_limit_wait_from_both_shapes(): assert (err.retry_after, err.reset_in_seconds) == (0, None) +def test_error_fields_follow_key_presence(): + err = map_response_to_error( + 429, json.dumps({"code": "rate_limited", "reset_in_seconds": None, "retry_after": 30}).encode() + ) + assert err.reset_in_seconds is None + body = {"code": "verification_failed", "hint": "", "failure": {"code": "x", "hint": "from the block"}} + assert map_response_to_error(409, json.dumps(body).encode()).hint == "" + + def test_quota_remaining_is_derived_when_absent(): legacy, canonical = _both("citecheck__402_no_credits.json") assert map_response_to_error(402, json.dumps(canonical).encode()).remaining == 100 assert map_response_to_error(402, json.dumps(legacy).encode()).remaining == 100 + assert map_response_to_error(402, json.dumps({"credits_remaining": 9, "cost": 1}).encode()).remaining is None # ── requests ── From d90b74ba2df6da1c261bbc95a768ae1db5878c86 Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 20:27:59 +0300 Subject: [PATCH 05/67] fix(webhooks): a newer-shape review or citecheck event reads its review_id / citecheck_id as task_id Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++-- src/lenz_io/webhooks.py | 11 +++++++---- tests/test_parity.py | 2 +- tests/test_read_both_shapes.py | 5 ++++- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02166a1..b4108b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,8 +39,9 @@ on upgrade. - Webhooks: an `event_id` property on every event, `VerificationFailed.failure`, `VerificationNeedsInput.reason` and `.claims` (properties: `dataclasses.asdict` and `repr` are unchanged). A review or citation-check - event in the newer shape carries no `task_id` (it reads `""`; deduplicate - on `event_id`, as before). `parse_webhook` and + event in the newer shape carries no `task_id`; it then reads the + `review_id` / `citecheck_id`, so code keyed on `task_id` keeps one key per + review (deduplicate deliveries on `event_id`, as before). `parse_webhook` and `LenzWebhooks.parse` read the newer envelope too (`event`, `event_id`, the work's id, `status`, and the polled body under `verification` / `review` / `citecheck`). diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index b1141ee..8d6bb54 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -229,7 +229,7 @@ class ReviewEvent(WebhookEvent): Deduplicate on ``event_id``: it is the same on every delivery attempt of one event, while ``attempt`` counts up. ``task_id`` identifies the delivery and cannot be polled on ``/verify/status``; the newer payload - shape leaves it out (it reads ``""``). The deep checks a + shape leaves it out (it then reads ``review_id``). The deep checks a review runs send no ``verification.*`` events of their own. """ @@ -248,7 +248,7 @@ class CitecheckEvent(WebhookEvent): still has it). Deduplicate on ``event_id``: it is the same on every delivery attempt of one event. ``task_id`` identifies the delivery and cannot be polled on ``/verify/status``; the newer payload shape leaves it - out (it reads ``""``). + out (it then reads ``citecheck_id``). """ event_id: str = "" @@ -360,7 +360,9 @@ def _build_event(raw: dict[str, Any]) -> WebhookEvent: review = None return ReviewEvent( event=event, - task_id=task_id, + # The newer payload carries no delivery ``task_id``: the review's + # id stands in, so code keyed on ``task_id`` keeps one key per review. + task_id=task_id if "task_id" in raw else str(payload.get("review_id") or ""), attempt=attempt, delivered_at=delivered_at, verification_id=verification_id, @@ -379,7 +381,8 @@ def _build_event(raw: dict[str, Any]) -> WebhookEvent: check = None return CitecheckEvent( event=event, - task_id=task_id, + # As on a review: the check's id stands in for the missing ``task_id``. + task_id=task_id if "task_id" in raw else str(payload.get("citecheck_id") or ""), attempt=attempt, delivered_at=delivered_at, verification_id=verification_id, diff --git a/tests/test_parity.py b/tests/test_parity.py index ee0b578..b398423 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -83,7 +83,7 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, if path == "dump.chain_id" and "chain_id" in lb and "chain_id" not in cb: return "`chain_id` is not in the newer shape" if path == "event.task_id" and "task_id" in lb and "task_id" not in cb: - return "review/citecheck webhooks drop `task_id` (never pollable); dedupe on `event_id`" + return "review/citecheck webhooks drop `task_id` (never pollable); it reads the review or check id" if path.startswith("event.result.") and name.startswith("webhook__verification_completed"): return "`result` is the dict as sent; the newer shape's carries `completed_at`" return KNOWN_GAPS.get((name, path)) diff --git a/tests/test_read_both_shapes.py b/tests/test_read_both_shapes.py index 7212622..a8741bb 100644 --- a/tests/test_read_both_shapes.py +++ b/tests/test_read_both_shapes.py @@ -355,7 +355,10 @@ def test_legacy_webhook_event_still_serialises(): def test_review_webhook_without_task_id(name): canonical = parse_webhook(load("canonical", name)["body"]) assert isinstance(canonical, (ReviewEvent, CitecheckEvent)) - assert canonical.task_id == "" and canonical.event_id != "" + work_id = canonical.review_id if isinstance(canonical, ReviewEvent) else canonical.citecheck_id + assert canonical.task_id == work_id != "" and canonical.event_id != "" + legacy = parse_webhook(load("legacy", name)["body"]) + assert legacy.task_id == load("legacy", name)["body"]["task_id"] # ── errors ── From d58a49a6119b2aa1a8bfe743203f3b92d7a4055b Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 22:32:43 +0300 Subject: [PATCH 06/67] feat!: ask for the API's current response shape (2026-10-11); 2.x attributes keep their values Every request now sends X-Lenz-API-Version: 2026-10-11. Every attribute the SDK had in 2.x reads the value it read then, computed from the current shape where the server renames or leaves a field out: - /assess: the no-claim error sentence, the compound-claim hint on a verdict row; review deep checks, issues and verification-stage failures spell "nothing checkable" not_a_claim. - /me/usage blocks projected with the original prices when none is published, quota_used never negative. - verification.completed webhook result: every original key and default. - Errors: a 422 keeps its original code, message and errors per endpoint (map_response_to_error takes the request's endpoint); a citation-check 402 reports credit_balance; TaskAccepted.chain_id reads "" when absent. The parity suite now holds every recorded response pair to the previous release's output with narrower allowances (only values the current shape cannot carry), and the citation 402 fixture follows the current server. BREAKING CHANGE: raw access (model_dump(), exc.body, event.raw, the CLI's --json) shows the current response shape. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 79 ++++++ src/lenz_io/client.py | 14 +- src/lenz_io/errors.py | 120 +++++++++- src/lenz_io/models.py | 86 +++++-- src/lenz_io/webhooks.py | 59 ++++- .../canonical/citecheck__402_no_credits.json | 2 +- tests/parity_observe.py | 23 +- tests/test_current_api_version.py | 225 ++++++++++++++++++ tests/test_parity.py | 35 +-- 9 files changed, 600 insertions(+), 43 deletions(-) create mode 100644 tests/test_current_api_version.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b4108b0..a368fed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,85 @@ All notable changes to this SDK are documented here. Format follows ## [Unreleased] +Major release (3.0.0). Code that reads the SDK's attributes keeps working +unchanged. Code that reads the raw response dict (`model_dump()`, +`exc.body`, a webhook event's `raw`, the CLI's `--json` output) sees the +API's current response shape: read the migration note below. + +### Changed + +- **The SDK asks for the API's current response shape.** Every request sends + `X-Lenz-API-Version: 2026-10-11` (`lenz_io.API_VERSION`; 2.x sent + `2026-05-13`). In that shape each field, status and error code has one name + across every endpoint. +- **Every attribute keeps the value it had in 2.x**, computed from the + current shape where the server now sends it under another name or not at + all. Among them: + - A failed `/assess` row still reads `verdict == "Error"` and + `confidence == "low"`, with `error_code` and `hint`; a verdict row that + found other claims carries its `hint` again. `AssessResponse.error` reads + `"No verifiable claim detected"` on an input with no claim. + - "Nothing checkable" keeps each field's old spelling: `no_claim` on + `/assess` rows, `AssessResponse.error_code`, review rows and the review's + own failure; `not_a_claim` on `/extract`, a verification + (`TaskStatus.failure_reason`, `LenzPipelineError.failure_reason`, the + `verification.failed` webhook's `error`) and a deep check inside a review. + - `modified_at` (set only when a verification completed on a later UTC + calendar day than it was created), `claim_text` on receipts, `text` on + `needs_input` options, `ExtractedClaims.claim` / `identified_claims` / + `locations`, `claim_limit_reached` / `citation_limit_reached`. + - `Usage.quota_resets_at`, `credits.bonus` and the `verify` / `ask` / + `assess` blocks, which the current `/me/usage` leaves out: projected from + `credits` and `costs` the way the API projected them. + - A `verification.completed` webhook's `result` has every key it had, with + the same defaults, plus `completed_at`. + - Errors: `reset_in_seconds` and `retry_after` on a 429, + `LenzQuotaExceededError.credit_balance` on a citation-check 402, and on a + 422 the original `code` (`blank_item` for a blank `/assess` item, + `validation_error` on `/review` and `/citecheck`), `message` (the list of + field errors for a request-schema failure; the parameter path on + `/review` and `/citecheck`) and `errors`. +- A repeated `verify` answered from the first submission is now a 202 (it was + a 200) without `chain_id`: `verify` returns the same `TaskAccepted`. +- `map_response_to_error` takes an optional `endpoint=(method, path)`, which + the client passes: a 422's original `code` and wording depend on it. + +### What reads differently + +The current shape does not carry a few values, so these attributes read the +server's current values: + +- A failed verification's sentence (`TaskStatus.error`, the message of the + `LenzPipelineError` that `wait` raises) is the current wording, e.g. "No + sources about the claim were found." where 2.x read "Pipeline stopped at: + research_empty". `failure_reason`, `failure_class` and `retryable` are + unchanged. +- Some error sentences are worded differently ("claim is required." for + "Text is required."). +- An error's `code` is set on every error, including those where 2.x read + `""` (401, 404, 405, 409 and some 422s); a 422 may list `errors` where 2.x + listed none; a failed verification may carry a `hint` where 2.x read `""`. +- `ExtractedClaims.status` reads `ready` when claims came back beside input + that held none (2.x: `not_a_claim`). +- A review's quick-check row whose passage held several claims has no `hint`. +- `TaskAccepted.chain_id` (an internal id, never pollable) reads `""`. + +### Migration + +- Code that reads attributes: nothing to do. +- Code that reads the raw dict: the keys are the current shape's. A model's + `model_dump()` holds what the server sent plus the 2.x fields filled in from + it; `exc.body` and `event.raw` hold the body as sent (`failure` blocks, + `claims` lists, `completed_at`, `more_claims`, `docs_url`, `retry_after`). + Read the attributes instead, or stay on 2.21 until you move. +- `webhook_url` keeps its meaning: on `verify` and `verify_batch` an empty + value (the default) means your key's default webhook and is not sent; on + `review` and `citecheck`, `None` means your key's default and `""` means no + webhook. In the current shape the API reads a missing `webhook_url` as the + key's default and `""` as no webhook on every endpoint. + +--- + Minor release (2.21.0). Existing code keeps working unchanged; nothing to do on upgrade. diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 999648f..5c005be 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -65,8 +65,8 @@ twice. Customer can override with explicit ``idempotency_key=...`` or opt out with ``idempotency=False``. ``ask.send`` takes the same argument but never generates one — see its docstring. -* ``X-Lenz-API-Version`` header pinned at SDK release date so the server - can route old clients to v1 handlers when v2 ships. +* ``X-Lenz-API-Version`` header pinned per SDK release (``API_VERSION``), so + the server answers every release in the response shape it was built for. * ``X-Request-ID`` is captured from every response onto the typed error for support escalation. """ @@ -125,9 +125,12 @@ logger = logging.getLogger("lenz_io") -# Pin the API version the SDK was built against. The server logs it on -# every request; when v2 ships, old SDKs keep getting v1 behavior. -API_VERSION = "2026-05-13" +# The API version this SDK asks for, sent as ``X-Lenz-API-Version`` on every +# request: the server answers in that version's response shape, whatever the +# account. Since 3.0.0 this is the current shape (one name for each field, +# status and error code). The models still read the original ``2026-05-13`` +# shape as well, and every attribute keeps the meaning it had in 2.x. +API_VERSION = "2026-10-11" DEFAULT_BASE_URL = "https://lenz.io/api/v1" DEFAULT_TIMEOUT = 30.0 @@ -1834,6 +1837,7 @@ def _request( response.status_code, response.content, dict(response.headers), + endpoint=(method, path), ) # Shouldn't reach here, but guard. diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 2f6579e..af4c839 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -20,6 +20,7 @@ from __future__ import annotations import json +import re import warnings from typing import TYPE_CHECKING, Any @@ -514,17 +515,122 @@ def _parse_body(raw: bytes | str | None) -> dict[str, Any]: return parsed if isinstance(parsed, dict) else {} +# ── The original 422 shapes, read from a newer-shape body ─────────────── +# +# The API's newer response shape gives every 422 one envelope: ``detail`` is a +# sentence, ``code`` is set and ``errors`` lists the fields. The original shape +# differed by endpoint, and a few codes were spelled differently. The +# exception's attributes keep their original values; ``exc.body`` is the body +# as sent. Each rule recognises only a newer-shape body, so an original-shape +# body reads exactly as before. + +#: /review and /citecheck (submit and read): one envelope in both shapes, +#: with the original ``code`` / ``detail`` wording below. +_REVIEW_FAMILY = re.compile(r"^/(?:review|reviews/[^/]+|citecheck|citechecks/[^/]+)$") +#: The codes a /review 422 renamed (the original said ``validation_error``). +_REVIEW_RENAMED_422 = ("blank_input", "unsupported_language") + + +def _items(parsed: dict[str, Any]) -> list[dict[str, Any]] | None: + errors = parsed.get("errors") + if not isinstance(errors, list) or not all(isinstance(i, dict) for i in errors): + return None + return errors + + +def _payload_loc(item: dict[str, Any]) -> bool: + """A field error from request-body validation (``body.payload...``).""" + loc = item.get("loc") + return isinstance(loc, list) and len(loc) >= 2 and loc[0] == "body" and loc[1] == "payload" + + +def _param_loc(item: dict[str, Any]) -> bool: + """A field error from query or path parameter validation.""" + loc = item.get("loc") + return isinstance(loc, list) and len(loc) >= 2 and loc[0] in ("query", "path") + + +def _review_family_original(parsed: dict[str, Any]) -> dict[str, Any]: + code, detail, items = parsed.get("code"), parsed.get("detail"), _items(parsed) + view = dict(parsed) + if code in _REVIEW_RENAMED_422: + view["code"] = "validation_error" + if code == "unsupported_language" and isinstance(detail, str) and not detail.startswith("language: "): + view["detail"] = f"language: {detail}" + if items: + first = items[0] + if _payload_loc(first): + # The original named the body parameter: ``payload.text: ...``. + loc = ".".join(str(p) for p in first["loc"] if p != "body") + view["detail"] = f"{loc}: {first.get('msg')}" + out = [] + for item in items: + item = {k: v for k, v in item.items() if k in ("loc", "msg")} + if view.get("detail") != detail and item.get("msg") == detail: + item["msg"] = view["detail"] + out.append(item) + view["errors"] = out + elif code == "idempotency_body_mismatch" and "errors" not in parsed: + view["errors"] = [{"loc": ["header"], "msg": detail}] + return view + + +def _original_422(parsed: dict[str, Any], method: str, path: str) -> dict[str, Any]: + """``parsed`` as the original response shape read, where the newer body + lets that be worked out; else ``parsed`` itself.""" + if not isinstance(parsed.get("detail"), str): + return parsed # the original shape's list ``detail``, or no detail + path = path.split("?", 1)[0] + if _REVIEW_FAMILY.match(path): + return _review_family_original(parsed) + code, items = parsed.get("code"), _items(parsed) + if not items: + return parsed + if ( + method.upper() == "POST" + and path == "/assess" + and code == "blank_input" + and isinstance(items[0].get("loc"), list) + and items[0]["loc"][:2] == ["body", "claims"] + and len(items[0]["loc"]) == 3 + ): + # A blank item in ``claims``: the original code was ``blank_item``. + return {**parsed, "code": "blank_item"} + if code == "validation_error" and all(_payload_loc(i) or _param_loc(i) for i in items): + if any(i.get("type") in (None, code) for i in items): + return parsed # the endpoint's own check, not request-schema validation + # Request-schema validation: the original ``detail`` was the list of + # field errors itself, each ``{type, loc, msg, ...}``. + listed = [] + for item in items: + ordered = {k: item[k] for k in ("type", "loc", "msg") if k in item} + ordered.update({k: v for k, v in item.items() if k not in ordered}) + listed.append(ordered) + return {**parsed, "detail": listed} + return parsed + + def map_response_to_error( status_code: int, body: bytes | str | None, headers: dict[str, str] | None = None, + *, + endpoint: tuple[str, str] | None = None, ) -> LenzError: """Translate an HTTP error response into the right typed exception. Returns an *instance* (not raised) so callers can decide whether to raise, log, or surface. Keep this pure — no I/O. + + ``endpoint`` is the request's ``(method, path)``: with it, a 422 in the + API's newer response shape gives the exception the same ``code``, + ``message`` and ``errors`` the original shape gave (``exc.body`` is + always the body as sent). """ - parsed = _parse_body(body) + raw = _parse_body(body) + parsed = raw + if status_code == 422 and endpoint is not None: + parsed = _original_422(raw, *endpoint) headers = headers or {} request_id = headers.get("X-Request-ID") or headers.get("x-request-id") or "" @@ -560,7 +666,7 @@ def map_response_to_error( request_id=request_id, status_code=status_code, code=code, - body=parsed, + body=raw, ) # Class-specific enrichment from the response body. Each is set on the @@ -640,6 +746,16 @@ def _read(key: str, block_key: str) -> Any: # NOT on the same-named deprecated property — that one aliases # ``remaining`` and means a different quantity (see the class docstring). err.credit_balance = _opt_int(parsed.get("credits_remaining")) + if ( + err.credit_balance is None + and err.remaining is not None + and "remaining" in parsed + and "docs_url" in parsed + and "doc_url" not in parsed + ): + # A newer-shape body leaves the pool out where it equals + # ``remaining`` (a citation check costs one credit). + err.credit_balance = err.remaining err.cost = _opt_int(parsed.get("cost")) resets_at = parsed.get("resets_at") err.resets_at = resets_at if isinstance(resets_at, str) and resets_at else None diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index c9fc1a9..7dfe457 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -402,7 +402,7 @@ class Verification(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - return _fill_modified_at(data) + return _deep_check_failure(_fill_modified_at(data)) @property def completed_at(self) -> str | None: @@ -439,7 +439,7 @@ class VerificationListItem(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - return _fill_modified_at(data) + return _deep_check_failure(_fill_modified_at(data)) @property def completed_at(self) -> str | None: @@ -612,6 +612,12 @@ def claims(self) -> list[ExtractedClaim]: return [ExtractedClaim(claim=n, positions=positions.get(n)) for n in names] +#: The original shape's ``hint`` on an /assess verdict row that found other +#: claims in its input, and its ``error`` on an input holding no claim. +_COMPOUND_HINT = "Assessed the main claim only. Send identified_claims as their own items to check the rest." +_NO_CLAIM_ERROR = "No verifiable claim detected" + + class AssessClaim(_Lax): """Per-claim entry in an ``AssessResponse.claims`` list. @@ -690,11 +696,17 @@ def _read_newer_shape(cls, data: Any) -> Any: if out.get("confidence") is None: out["confidence"] = "low" failure = out.get("failure") if isinstance(out.get("failure"), dict) else {} + more = out.get("more_claims") or [] + if failure: + hint = failure.get("hint") + else: + # A verdict row with other claims found carried this one sentence. + hint = _COMPOUND_HINT if more else None return _fill( out, error_code=_old_code(failure.get("code"), "no_claim") if failure else None, - hint=failure.get("hint") if failure else None, - identified_claims=out.get("more_claims") or [], + hint=hint, + identified_claims=more, ) @property @@ -778,7 +790,8 @@ def _read_newer_shape(cls, data: Any) -> Any: return data return _fill( data, - error=failure.get("detail"), + # The original shape's one sentence for an input with no claim. + error=_NO_CLAIM_ERROR, error_code=_old_code(failure.get("code"), "no_claim") or "", ) @@ -829,6 +842,14 @@ def claim(self) -> str: sent = _sent(self, "claim") return sent if isinstance(sent, str) else self.claim_text + @property + def chain_id(self) -> str: + """An internal correlation id the original response shape sent on a + ``verify`` receipt; ``""`` when the response carries none (the current + shape never does). It cannot be polled: use ``task_id``.""" + sent = _sent(self, "chain_id") + return sent if isinstance(sent, str) else "" + class BatchAccepted(_Lax): batch_id: str = "" @@ -1143,6 +1164,11 @@ class UsageExtract(_Lax): unlimited: bool = False +#: The per-capability blocks the original /me/usage carried, with the price +#: each was projected at when a response publishes none. +_BLOCK_COSTS = (("verify", 10), ("ask", 1), ("assess", 1)) + + class Usage(_Lax): """Returned by ``GET /me/usage`` — the account's balance and what it buys. @@ -1242,20 +1268,21 @@ def _int(value: Any) -> int: out = _fill(data, quota_resets_at=credits.get("resets_at")) if not isinstance(costs, dict): - return out + costs = {} total, remaining = _int(credits.get("total")), _int(credits.get("remaining")) extra = _int(credits.get("extra", credits.get("bonus"))) - for capability in ("verify", "ask", "assess"): - cost = _int(costs.get(capability)) - if cost > 0: - quota_total, left = total // cost, remaining // cost - out[capability] = { - "quota_used": quota_total - left, - "quota_total": quota_total, - "quota_remaining": left, - "bonus": extra // cost, - "remaining": left, - } + for capability, default_cost in _BLOCK_COSTS: + # At the price the body publishes; a capability it leaves out (or + # prices at 0) at the price the blocks always used. + cost = _int(costs.get(capability)) or default_cost + quota_total, left = total // cost, remaining // cost + out[capability] = { + "quota_used": max(0, quota_total - left), + "quota_total": quota_total, + "quota_remaining": left, + "bonus": extra // cost, + "remaining": left, + } return out @@ -1364,6 +1391,16 @@ def detail(self) -> str | None: return sent if isinstance(sent, str) else None +def _deep_check_failure(data: Any) -> Any: + """A newer-shape ``failure`` block of a deep check inside a review: its + "nothing checkable" was ``not_a_claim`` (a quick check's and the review's + own, ``no_claim``, which ``FailureBlock`` reads by default).""" + failure = data.get("failure") if isinstance(data, dict) else None + if isinstance(failure, dict) and _is_newer(failure, "code", "failure_reason"): + return {**data, "failure": _fill(failure, failure_reason=_old_code(failure["code"], "not_a_claim"))} + return data + + class EscalationPolicy(_Lax): """Which quick-checked claims get a deep check, as the review RESOLVED it. @@ -1596,7 +1633,7 @@ class ReviewVerification(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - return _fill_modified_at(data) + return _deep_check_failure(_fill_modified_at(data)) @property def completed_at(self) -> str | None: @@ -1709,6 +1746,12 @@ class ReviewIssue(_Lax): #: A copy of its claim row's ``suggested_edits``. suggested_edits: SuggestedEdits | None = None + @model_validator(mode="before") + @classmethod + def _read_newer_shape(cls, data: Any) -> Any: + # An issue's failure is its deep check's. + return _deep_check_failure(data) + class ReviewFailure(_Lax): """A claim outside the issues whose work failed. ``stage`` is @@ -1719,6 +1762,13 @@ class ReviewFailure(_Lax): stage: str = "" failure: FailureBlock | None = None + @model_validator(mode="before") + @classmethod + def _read_newer_shape(cls, data: Any) -> Any: + if isinstance(data, dict) and data.get("stage") == "verification": + return _deep_check_failure(data) + return data + class ReviewCitationResult(_Lax): """The one answer to read for a citation, derived from its ``check``. diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index 8d6bb54..17ab451 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -27,6 +27,7 @@ from __future__ import annotations +import copy import hashlib import hmac import json @@ -256,6 +257,62 @@ class CitecheckEvent(WebhookEvent): citecheck: Citecheck | None = None +#: The original ``verification.completed`` ``result``: every key, in order, +#: with the value it took when the verification left it out. A newer payload +#: sends only the keys it has. +_RESULT_DEFAULTS: tuple[tuple[str, Any], ...] = ( + ("verification_id", ""), + ("claim", ""), + ("visibility", "private"), + ("depth", "standard"), + ("domain", ""), + ("entities", []), + ("presumed_intent", ""), + ("verdict", ""), + ("confidence", "low"), + ("lenz_score", None), + ("key_finding", ""), + ("executive_summary", ""), + ("warnings", []), + ("suggested_rewrite", None), + ("created_at", ""), + ("modified_at", None), + ("sources", []), + ("audit", None), + ("language", "en"), + ("coverage", None), +) +_AUDIT_DEFAULTS: tuple[tuple[str, Any], ...] = ( + ("adjudication_summary", ""), + ("assessments", []), + ("debate_pro", None), + ("debate_con", None), + ("panel_agreement", ""), +) +_SIDE_DEFAULTS: tuple[tuple[str, Any], ...] = (("role", ""), ("argument", ""), ("rebuttal", "")) + + +def _with_defaults(value: Any, defaults: tuple[tuple[str, Any], ...]) -> dict[str, Any]: + """``value`` (a dict, else empty) with every key of ``defaults`` in that + order, then any key it carries beyond them.""" + given = value if isinstance(value, dict) else {} + out = {key: given.get(key, copy.deepcopy(default)) for key, default in defaults} + out.update({k: v for k, v in given.items() if k not in out}) + return out + + +def _original_result(result: dict[str, Any]) -> dict[str, Any]: + """A newer payload's ``result`` with the original's keys and defaults + (``modified_at`` computed from ``completed_at``); keys the original did + not have (``completed_at``) follow them.""" + out = _with_defaults(_fill_modified_at(result), _RESULT_DEFAULTS) + audit = _with_defaults(out["audit"], _AUDIT_DEFAULTS) + audit["debate_pro"] = _with_defaults(audit["debate_pro"], _SIDE_DEFAULTS) + audit["debate_con"] = _with_defaults(audit["debate_con"], _SIDE_DEFAULTS) + out["audit"] = audit + return out + + def _original_view(payload: dict[str, Any]) -> dict[str, Any]: """The original flat ``verification.*`` fields, for a payload in the newer envelope (the polled body nested under ``verification``). Any other @@ -267,7 +324,7 @@ def _original_view(payload: dict[str, Any]) -> dict[str, Any]: view.setdefault("task_id", body.get("task_id")) result = body.get("result") if isinstance(result, dict): - view.setdefault("result", _fill_modified_at(result)) + view.setdefault("result", _original_result(result)) view.setdefault("verification_id", result.get("verification_id")) failure = body.get("failure") if isinstance(failure, dict): diff --git a/tests/fixtures/parity/canonical/citecheck__402_no_credits.json b/tests/fixtures/parity/canonical/citecheck__402_no_credits.json index 5d36059..69c2002 100644 --- a/tests/fixtures/parity/canonical/citecheck__402_no_credits.json +++ b/tests/fixtures/parity/canonical/citecheck__402_no_credits.json @@ -6,8 +6,8 @@ "docs_url": "https://lenz.io/docs/errors#quota", "upgrade_url": "https://lenz.io/plans?wall=6b86b273-ff34-4ce1-8d6b-804eff5a3f57", "wall_id": "6b86b273-ff34-4ce1-8d6b-804eff5a3f57", + "remaining": 100, "resets_at": "2026-10-01T14:07:00+00:00", - "credits_remaining": 100, "cost": 1 }, "headers": { diff --git a/tests/parity_observe.py b/tests/parity_observe.py index 0ed28d7..e60c4c6 100644 --- a/tests/parity_observe.py +++ b/tests/parity_observe.py @@ -12,6 +12,7 @@ from __future__ import annotations import contextlib +import inspect import io import json import warnings @@ -136,6 +137,21 @@ def _error_view(err: BaseException) -> dict[str, Any]: return view +def endpoint_for(name: str) -> tuple[str, str]: + """The ``(method, path)`` a recorded error answered, as precisely as the + error mapping needs it: /review and /citecheck, /assess, or any other.""" + area, case = name.split("__", 1) + if area == "review": + return ("GET", "/reviews/r1") if case.startswith("get_") else ("POST", "/review") + if area == "citecheck": + return ("GET", "/citechecks/c1") if case.startswith("get_") else ("POST", "/citecheck") + if area == "assess": + return ("POST", "/assess") + if area == "extract": + return ("POST", "/extract") + return ("POST", "/verify") + + def _model_for(name: str) -> Any: area = name.split("__", 1)[0] case = name.split("__", 1)[1] @@ -180,7 +196,12 @@ def observe(name: str, fixture: dict[str, Any]) -> dict[str, Any]: result["event"] = view return result if status >= 400: - err = errors_mod.map_response_to_error(status, json.dumps(body).encode(), headers) + kwargs: dict[str, Any] = {} + if "endpoint" in inspect.signature(errors_mod.map_response_to_error).parameters: + # The request the client sends, as far as the mapping reads it + # (releases before the newer shape take no endpoint). + kwargs["endpoint"] = endpoint_for(name) + err = errors_mod.map_response_to_error(status, json.dumps(body).encode(), headers, **kwargs) result["error"] = _error_view(err) return result cls = _model_for(name) diff --git a/tests/test_current_api_version.py b/tests/test_current_api_version.py new file mode 100644 index 0000000..830c29e --- /dev/null +++ b/tests/test_current_api_version.py @@ -0,0 +1,225 @@ +"""3.0.0 asks for the API's current response shape (``2026-10-11``) and keeps +every 2.x attribute's meaning. + +``test_parity.py`` runs every recorded response pair through the models, the +error mapping, the CLI and the webhook parser. This file covers what the pair +fixtures cannot show on their own: the header the client sends, the request +bodies that depend on it, and the error mapping as the client drives it (the +request's endpoint decides a 422's original ``code`` and wording). +""" + +from __future__ import annotations + +import json +import warnings +from typing import Any + +import pytest +import respx +from parity_observe import load + +from lenz_io import Lenz +from lenz_io.client import API_VERSION, DEFAULT_BASE_URL +from lenz_io.errors import LenzQuotaExceededError, LenzValidationError +from lenz_io.models import AssessResponse, ReviewVerification, Usage +from lenz_io.webhooks import VerificationCompleted, parse_webhook + +KEY = "lenz_" + "0" * 32 + + +@pytest.fixture +def client() -> Any: + c = Lenz(api_key=KEY, max_retries=0) + yield c + c.close() + + +def _canonical(name: str) -> dict[str, Any]: + return load("canonical", name) + + +# ── The version the client asks for ── + + +def test_the_client_asks_for_the_current_shape(client: Lenz) -> None: + assert API_VERSION == "2026-10-11" + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + route = mock.get("/me/usage").respond(200, json=_canonical("account__me_usage_free.json")["body"]) + client.usage() + assert route.calls.last.request.headers["X-Lenz-API-Version"] == "2026-10-11" + + +# ── Request bodies: ``webhook_url`` keeps its 2.x meaning ── +# +# In the current shape ``webhook_url`` means the same on every endpoint: +# left out (or null) = the key's default webhook, ``""`` = no webhook for this +# request. The SDK's parameters keep what they meant in 2.x. + + +def _sent(route: Any) -> dict[str, Any]: + return json.loads(route.calls.last.request.content) + + +def test_verify_without_a_webhook_url_leaves_it_out(client: Lenz) -> None: + """2.x: ``webhook_url=""`` (the default) meant the key's default webhook.""" + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + route = mock.post("/verify").respond(202, json={"task_id": "t" * 32, "status": "queued"}) + client.verify("The Earth is round.") + client.verify("The Earth is round.", webhook_url="") + assert "webhook_url" not in _sent(route) + + +def test_verify_batch_leaves_out_every_empty_webhook_url(client: Lenz) -> None: + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + route = mock.post("/verify/batch").respond(202, json={"batch_id": "b", "items": []}) + client.verify_batch( + claims=[{"claim": "A.", "webhook_url": ""}, {"text": "B.", "webhook_url": "https://h.test/x"}] + ) + body = _sent(route) + assert "webhook_url" not in body + assert "webhook_url" not in body["claims"][0] + assert body["claims"][1]["webhook_url"] == "https://h.test/x" + + +@pytest.mark.parametrize( + ("method", "path", "receipt"), + [ + ("review", "/review", {"review_id": "r1", "status": "queued"}), + ("citecheck", "/citecheck", {"citecheck_id": "c1", "status": "queued"}), + ], +) +def test_review_and_citecheck_keep_their_webhook_url_meaning( + client: Lenz, method: str, path: str, receipt: dict[str, Any] +) -> None: + """2.x: ``None`` = the key's default (left out), ``""`` = no webhook + (sent as ``""``, which means the same in the current shape).""" + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + route = mock.post(path).respond(202, json=receipt) + getattr(client, method)("Draft text with a claim.") + assert "webhook_url" not in _sent(route) + getattr(client, method)("Draft text with a claim.", webhook_url="") + assert _sent(route)["webhook_url"] == "" + + +def test_a_202_receipt_reads_as_the_200_did(client: Lenz) -> None: + """A repeated /verify answered from the first submission is a 202 now (a + 200 before), without ``chain_id``: the SDK returns the same receipt.""" + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.post("/verify").respond(202, json=_canonical("verify__implicit_repeat_replay.json")["body"]) + receipt = client.verify("The Earth is round.") + assert receipt.task_id == _canonical("verify__implicit_repeat_replay.json")["body"]["task_id"] + + +# ── Errors, as the client maps them ── + + +def _raise(client: Lenz, method: str, path: str, fixture: str, call: Any) -> Any: + response = _canonical(fixture) + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.route(method=method, path=path).respond(response["status"], json=response["body"]) + with pytest.raises(Exception) as caught: + call() + return caught.value + + +def test_a_review_422_keeps_its_original_code_and_wording(client: Lenz) -> None: + err = _raise(client, "POST", "/review", "review__422_unsupported_language.json", lambda: client.review("Draft.")) + original = load("legacy", "review__422_unsupported_language.json")["body"] + assert isinstance(err, LenzValidationError) + assert err.code == "validation_error" == original["code"] + assert err.message == original["detail"] + assert err.errors == original["errors"] + # The body as sent is still there. + assert err.body["code"] == "unsupported_language" + + +def test_a_review_schema_422_names_the_body_parameter_as_before(client: Lenz) -> None: + err = _raise(client, "POST", "/review", "review__422_depth_unknown.json", lambda: client.review("Draft.")) + original = load("legacy", "review__422_depth_unknown.json")["body"] + assert err.message == original["detail"] + assert err.errors == original["errors"] + + +def test_a_blank_assess_item_keeps_blank_item(client: Lenz) -> None: + err = _raise(client, "POST", "/assess", "assess__422_blank_item.json", lambda: client.assess(claims=["A.", " "])) + assert err.code == "blank_item" + + +def test_a_schema_422_lists_its_field_errors_as_the_message(client: Lenz) -> None: + """The original shape's ``detail`` was the list itself; the exception's + message and ``errors`` read exactly as they did.""" + err = _raise(client, "POST", "/verify", "verify__invalid_depth_422.json", lambda: client.verify("A claim.")) + original = load("legacy", "verify__invalid_depth_422.json")["body"]["detail"] + assert err.message == str(original) + assert [list(i) for i in err.errors] == [list(i) for i in original] + + +def test_a_citation_402_reports_the_credit_pool(client: Lenz) -> None: + err = _raise(client, "POST", "/citecheck", "citecheck__402_no_credits.json", lambda: client.citecheck("Draft.")) + assert isinstance(err, LenzQuotaExceededError) + assert (err.remaining, err.credit_balance, err.cost) == (100, 100, 1) + + +# ── Values the current shape leaves out, rebuilt with their 2.x meaning ── + + +def test_a_no_claim_assess_keeps_its_original_error_sentence() -> None: + out = AssessResponse.model_validate(_canonical("assess__single_no_claim.json")["body"]) + assert out.error == "No verifiable claim detected" + assert out.error_code == "no_claim" + + +def test_a_failed_deep_check_in_a_review_says_not_a_claim() -> None: + row = ReviewVerification.model_validate( + {"status": "failed", "failure": {"code": "no_checkable_claim", "detail": "x", "hint": None}} + ) + assert row.failure is not None + assert row.failure.failure_reason == "not_a_claim" + assert row.failure.code == "no_checkable_claim" + + +def test_usage_blocks_without_published_prices_use_the_original_ones() -> None: + usage = Usage.model_validate({"plan": "free", "credits": {"total": 100, "used": 0, "remaining": 100, "extra": 5}}) + assert usage.verify.quota_total == 10 + assert usage.ask.quota_total == 100 + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + assert usage.assess.credits == 5 + + +def test_usage_quota_used_never_goes_below_zero() -> None: + usage = Usage.model_validate( + {"plan": "free", "credits": {"total": 5, "used": 0, "remaining": 25, "extra": 20}, "costs": {"verify": 10}} + ) + assert usage.verify.quota_used == 0 + + +def test_a_sparse_completed_webhook_result_has_every_original_key() -> None: + event = parse_webhook(_canonical("webhook__verification_completed_sparse_result.json")["body"]) + original = parse_webhook(load("legacy", "webhook__verification_completed_sparse_result.json")["body"]) + assert isinstance(event, VerificationCompleted) + assert isinstance(original, VerificationCompleted) + assert event.result == original.result + assert list(event.result) == list(original.result) + + +def test_a_failed_deep_check_says_not_a_claim_on_issues_and_failures() -> None: + from lenz_io.models import ReviewFailure, ReviewIssue + + block = {"code": "no_checkable_claim", "detail": "x", "hint": None} + issue = ReviewIssue.model_validate({"claim_index": 0, "verdict": "Mixed", "failure": dict(block)}) + deep = ReviewFailure.model_validate({"claim_index": 0, "stage": "verification", "failure": dict(block)}) + quick = ReviewFailure.model_validate({"claim_index": 0, "stage": "assessment", "failure": dict(block)}) + assert issue.failure is not None and issue.failure.failure_reason == "not_a_claim" + assert deep.failure is not None and deep.failure.failure_reason == "not_a_claim" + assert quick.failure is not None and quick.failure.failure_reason == "no_claim" + + +def test_a_receipt_without_chain_id_reads_it_as_empty() -> None: + from lenz_io.models import TaskAccepted + + current = TaskAccepted.model_validate(_canonical("verify__submit_202.json")["body"]) + original = TaskAccepted.model_validate(load("legacy", "verify__submit_202.json")["body"]) + assert current.chain_id == "" + assert original.chain_id == load("legacy", "verify__submit_202.json")["body"]["chain_id"] + assert "chain_id" in original.model_dump() diff --git a/tests/test_parity.py b/tests/test_parity.py index b398423..a2abb2c 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -63,38 +63,43 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, lb, cb = legacy["body"], canonical["body"] head, _, rest = path.partition(".") if head == "error": - if rest in _MESSAGE_PATHS and lb.get("detail") != cb.get("detail"): - return "the server words `detail` differently (a 422 sends a sentence, not the list)" - if rest == "code" and lb.get("code") != cb.get("code"): - return "the server sends a different (or a first) `code`" - if rest.startswith("errors") and _validation_items(lb) != _validation_items(cb): - return "the server's field errors differ" + legacy_sentence = isinstance(lb.get("detail"), str) + if rest in _MESSAGE_PATHS and legacy_sentence and lb.get("detail") != cb.get("detail"): + return "the server words its `detail` sentence differently" + if rest in _MESSAGE_PATHS and "detail" not in lb and isinstance(cb.get("detail"), str): + return "the newer shape sends a `detail` sentence where the original sent none" + if rest == "code" and "code" not in lb and "code" in cb: + return "the newer shape names a `code` where the original sent none" + if rest.startswith("errors") and not _validation_items(lb) and _validation_items(cb): + return "the newer shape lists the field errors where the original listed none" if rest in ("hint", "fix", "friendly_text", "payload_json.error.fix") and lb.get("hint") != cb.get("hint"): return "the server words the hint differently" if head == "wait" or path in ("dump.error", "render"): sentence = lb.get("error") - if isinstance(sentence, str) and sentence != _failure(cb).get("detail"): + if ( + name.startswith("verify__") + and isinstance(sentence, str) + and sentence != _failure(cb).get("detail") + and "failure" in cb + ): if path in ("dump.error", "render") or rest in _MESSAGE_PATHS: return "the failed run's sentence: the newer shape's `failure.detail` is worded differently" if path in ("dump.hint", "wait.hint") and lb.get("hint", "") != _failure(cb).get("hint", ""): return "the newer shape carries a hint the original did not" - if path.startswith("wait.payload.") and name.startswith("verify__status_needs_input"): - return "LenzNeedsInputError.payload is the status as sent (model_dump)" + option_claim = path.startswith("wait.payload.claims[") and path.endswith("].claim") + if option_claim and name.startswith("verify__status_needs_input"): + return "LenzNeedsInputError.payload is the status dump: an option also carries its newer name `claim`" if path == "dump.chain_id" and "chain_id" in lb and "chain_id" not in cb: return "`chain_id` is not in the newer shape" if path == "event.task_id" and "task_id" in lb and "task_id" not in cb: return "review/citecheck webhooks drop `task_id` (never pollable); it reads the review or check id" - if path.startswith("event.result.") and name.startswith("webhook__verification_completed"): - return "`result` is the dict as sent; the newer shape's carries `completed_at`" + if path == "event.result.completed_at" and name.startswith("webhook__verification_completed"): + return "`result` also carries the newer `completed_at` beside `modified_at`" return KNOWN_GAPS.get((name, path)) #: Single gaps no rule covers, each with its reason. KNOWN_GAPS: dict[tuple[str, str], str] = { - ("assess__list_compound_item.json", "dump.claims[0].hint"): ( - "the newer shape sends no hint on a completed row with other claims found" - ), - ("assess__list_compound_item.json", "render"): "the same hint line, printed by the CLI", ("review__get_assessment_rows_full_fields.json", "dump.claims[0].assessment.hint"): ( "the newer shape sends no hint on a completed quick check with other claims found" ), From 67cf2d39a100f40da0453b605dc5646fc46c5d98 Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 22:35:53 +0300 Subject: [PATCH 07/67] fix(errors): a request-schema 422 reads no code, a blank /assess item no field errors, as in 2.x Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/errors.py | 9 +++++---- tests/test_current_api_version.py | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index af4c839..7ea1028 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -594,19 +594,20 @@ def _original_422(parsed: dict[str, Any], method: str, path: str) -> dict[str, A and items[0]["loc"][:2] == ["body", "claims"] and len(items[0]["loc"]) == 3 ): - # A blank item in ``claims``: the original code was ``blank_item``. - return {**parsed, "code": "blank_item"} + # A blank item in ``claims``: the original said ``blank_item`` and + # listed no field errors. + return {k: v for k, v in {**parsed, "code": "blank_item"}.items() if k != "errors"} if code == "validation_error" and all(_payload_loc(i) or _param_loc(i) for i in items): if any(i.get("type") in (None, code) for i in items): return parsed # the endpoint's own check, not request-schema validation # Request-schema validation: the original ``detail`` was the list of - # field errors itself, each ``{type, loc, msg, ...}``. + # field errors itself, each ``{type, loc, msg, ...}``, with no ``code``. listed = [] for item in items: ordered = {k: item[k] for k in ("type", "loc", "msg") if k in item} ordered.update({k: v for k, v in item.items() if k not in ordered}) listed.append(ordered) - return {**parsed, "detail": listed} + return {k: v for k, v in {**parsed, "detail": listed}.items() if k not in ("code", "errors")} return parsed diff --git a/tests/test_current_api_version.py b/tests/test_current_api_version.py index 830c29e..220ef4e 100644 --- a/tests/test_current_api_version.py +++ b/tests/test_current_api_version.py @@ -143,6 +143,7 @@ def test_a_review_schema_422_names_the_body_parameter_as_before(client: Lenz) -> def test_a_blank_assess_item_keeps_blank_item(client: Lenz) -> None: err = _raise(client, "POST", "/assess", "assess__422_blank_item.json", lambda: client.assess(claims=["A.", " "])) assert err.code == "blank_item" + assert err.errors == [] def test_a_schema_422_lists_its_field_errors_as_the_message(client: Lenz) -> None: @@ -151,6 +152,7 @@ def test_a_schema_422_lists_its_field_errors_as_the_message(client: Lenz) -> Non err = _raise(client, "POST", "/verify", "verify__invalid_depth_422.json", lambda: client.verify("A claim.")) original = load("legacy", "verify__invalid_depth_422.json")["body"]["detail"] assert err.message == str(original) + assert err.code == "" assert [list(i) for i in err.errors] == [list(i) for i in original] From 36877b4c449f5d7b6d287d6d79cbfb0bb13adc4a Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 22:43:39 +0300 Subject: [PATCH 08/67] fix(verify): a whitespace-only webhook_url is left out; changelog covers webhook versions and every value that reads differently On verify and verify_batch a blank or whitespace-only webhook_url (and a None) meant the key's default webhook in 2.x; the current API version reads a blank value as "no webhook", so the SDK leaves it out. The 3.0.0 changelog now says that work submitted with 3.0 sends its webhooks in the current shape (receivers must run 2.21+ first), and lists the 409 hint, the 400 codes, older stored review rows and the review/citecheck webhook task_id among the values that read differently. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 30 +++++++++++++++++++++++++++--- src/lenz_io/client.py | 15 ++++++++++++--- tests/test_current_api_version.py | 16 ++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a368fed..e9e4649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,15 +62,38 @@ server's current values: - Some error sentences are worded differently ("claim is required." for "Text is required."). - An error's `code` is set on every error, including those where 2.x read - `""` (401, 404, 405, 409 and some 422s); a 422 may list `errors` where 2.x - listed none; a failed verification may carry a `hint` where 2.x read `""`. + `""` (400s such as `malformed_body` and `verification_not_ready` from + `ask.send`, 401, 404, 405, 409 and some 422s); a 422 may list `errors` + where 2.x listed none; a failed verification may carry a `hint` where 2.x + read `""`. +- `verifications.get` on a run that failed (409 `verification_failed`): the + `LenzPipelineError`'s `hint` and `fix`, and the CLI's message, carry the + run's own advice where it has one (e.g. "Send one factual claim, ...") where + 2.x sometimes read "Resubmit to POST /verify if retryable is true. ...". - `ExtractedClaims.status` reads `ready` when claims came back beside input that held none (2.x: `not_a_claim`). - A review's quick-check row whose passage held several claims has no `hint`. +- Reviews stored before the API's current shape: a quick-check row or a + `ReviewFailure` that 2.x read with `failure` `None` may read a failure block, + and a deep check's `modified_at` is computed from its completion time + (set only when it completed on a later UTC calendar day) rather than read + as stored. +- A `review.*` / `citecheck.*` webhook event's `task_id` reads the review or + check id (the payload no longer carries the delivery id; deduplicate on + `event_id`). - `TaskAccepted.chain_id` (an internal id, never pollable) reads `""`. ### Migration +- **Webhooks follow the version of the request that submitted the work.** A + `verify`, `verify_batch`, `select`, `review` or `citecheck` call made with + 3.0 gets its webhooks in the current shape (one envelope: `event`, + `event_id`, the work's id, `status`, and the polled body under + `verification` / `review` / `citecheck`). If the service that RECEIVES your + webhooks parses them with lenz-io 2.20 or older, or reads the raw JSON, + upgrade it to 2.21 or later (which reads both shapes) before the service + that SENDS requests moves to 3.0. Work submitted with 2.x keeps sending + the original shape. - Code that reads attributes: nothing to do. - Code that reads the raw dict: the keys are the current shape's. A model's `model_dump()` holds what the server sent plus the 2.x fields filled in from @@ -78,7 +101,8 @@ server's current values: `claims` lists, `completed_at`, `more_claims`, `docs_url`, `retry_after`). Read the attributes instead, or stay on 2.21 until you move. - `webhook_url` keeps its meaning: on `verify` and `verify_batch` an empty - value (the default) means your key's default webhook and is not sent; on + or whitespace-only value (the default is `""`) means your key's default + webhook and is not sent; on `review` and `citecheck`, `None` means your key's default and `""` means no webhook. In the current shape the API reads a missing `webhook_url` as the key's default and `""` as no webhook on every endpoint. diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 5c005be..3894fad 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -199,6 +199,15 @@ def _user_agent() -> str: return f"lenz-io-python/{__version__} (httpx {httpx.__version__})" +def _blank_webhook_url(value: Any) -> bool: + """An empty or whitespace-only ``webhook_url`` on ``verify`` / + ``verify_batch``: it always meant the key's default webhook (the API read + it as left out), so it is left out, which keeps that meaning on every API + version (the current one reads a blank value as "no webhook"). ``None`` + is left out too.""" + return value is None or (isinstance(value, str) and not value.strip()) + + def _batch_item_body(item: Any) -> Any: """The wire shape of one batch item. @@ -209,7 +218,7 @@ def _batch_item_body(item: Any) -> Any: the key's default, and leaving it out keeps that meaning on every API version. """ - if isinstance(item, dict) and item.get("webhook_url") == "": + if isinstance(item, dict) and _blank_webhook_url(item.get("webhook_url")): item = {k: v for k, v in item.items() if k != "webhook_url"} if "claim" not in item: return item @@ -1590,7 +1599,7 @@ def _verify_submit( # Omit-when-empty: no ``webhook_url`` means the key's default webhook. # An empty string is never sent, so a request keeps that meaning on # every API version (a newer one reads ``""`` as "no webhook"). - if webhook_url: + if not _blank_webhook_url(webhook_url): payload["webhook_url"] = webhook_url # Omit-when-empty so existing English callers keep byte-identical # request bodies (no extra "language": "" key). @@ -1626,7 +1635,7 @@ def _verify_batch( # runtime contract a plain dict). payload: dict[str, Any] = {"claims": [_batch_item_body(c) for c in claims]} # Omit-when-empty, as on ``verify``: no ``webhook_url`` means the key's default. - if webhook_url: + if not _blank_webhook_url(webhook_url): payload["webhook_url"] = webhook_url if language: payload["language"] = language diff --git a/tests/test_current_api_version.py b/tests/test_current_api_version.py index 220ef4e..657f25a 100644 --- a/tests/test_current_api_version.py +++ b/tests/test_current_api_version.py @@ -225,3 +225,19 @@ def test_a_receipt_without_chain_id_reads_it_as_empty() -> None: assert current.chain_id == "" assert original.chain_id == load("legacy", "verify__submit_202.json")["body"]["chain_id"] assert "chain_id" in original.model_dump() + + +@pytest.mark.parametrize("blank", ["", " ", "\t\n", None]) +def test_a_blank_verify_webhook_url_is_left_out(client: Lenz, blank: Any) -> None: + """2.x: the API read a blank ``webhook_url`` on /verify as left out, so + the key's default webhook fired. The current shape reads a blank value as + "no webhook", so the SDK leaves it out to keep the 2.x meaning.""" + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + single = mock.post("/verify").respond(202, json={"task_id": "t" * 32, "status": "queued"}) + batch = mock.post("/verify/batch").respond(202, json={"batch_id": "b", "items": []}) + client.verify("The Earth is round.", webhook_url=blank) + client.verify_batch(claims=[{"claim": "A.", "webhook_url": blank}], webhook_url=blank) + assert "webhook_url" not in _sent(single) + body = _sent(batch) + assert "webhook_url" not in body + assert "webhook_url" not in body["claims"][0] From b458f5002da1903eabf600249bed01312c081732 Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 22:55:15 +0300 Subject: [PATCH 09/67] fix: match the Node SDK: 2.x error codes per endpoint, a failed poll's 2.x sentence - Errors: the whole original error body is rebuilt per endpoint, as the Node SDK does: `code` is "" where the 2.x error carried none (the API now sends one on every error), plus the 422 envelopes, original wait and link names, and a citation-check 402's pool balance. `exc.body` is the body as sent. - A failed poll's `error` (and the LenzPipelineError message) reads its 2.x sentence, rebuilt from the code: the fixed sentence for cancelled, task_stuck, task_error and not_a_claim, else "Pipeline stopped at: ". - The parity suite no longer allows a new error code; the changelog's list of what reads differently matches the Node SDK's. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 79 ++++++----- src/lenz_io/errors.py | 213 ++++++++++++++++++------------ src/lenz_io/models.py | 25 +++- tests/parity_observe.py | 2 + tests/test_current_api_version.py | 42 ++++++ tests/test_parity.py | 14 +- tests/test_read_both_shapes.py | 5 +- 7 files changed, 244 insertions(+), 136 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e4649..706a193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,50 +38,49 @@ API's current response shape: read the migration note below. `credits` and `costs` the way the API projected them. - A `verification.completed` webhook's `result` has every key it had, with the same defaults, plus `completed_at`. - - Errors: `reset_in_seconds` and `retry_after` on a 429, - `LenzQuotaExceededError.credit_balance` on a citation-check 402, and on a - 422 the original `code` (`blank_item` for a blank `/assess` item, - `validation_error` on `/review` and `/citecheck`), `message` (the list of - field errors for a request-schema failure; the parameter path on - `/review` and `/citecheck`) and `errors`. -- A repeated `verify` answered from the first submission is now a 202 (it was - a 200) without `chain_id`: `verify` returns the same `TaskAccepted`. + - A failed `get_status` / `wait`: `error` reads its 2.x sentence, rebuilt + from the failure code (the fixed sentence for `cancelled`, `task_stuck`, + `task_error` and `not_a_claim`, else "Pipeline stopped at: "), so + the `LenzPipelineError` message reads as before; `failure_reason`, + `failure_class`, `retryable`, `docs_url` and `hint` too. + - Errors: every class and attribute as 2.x set it. `code` is `""` where the + 2.x error carried none (the API now sends one on every error: + `not_authenticated`, `not_found`, `validation_error`, `malformed_body`, + `verification_not_ready` from `ask.send`, ...), per endpoint; a 422 keeps + its 2.x `code` (`blank_item` for a blank `/assess` item, + `validation_error` on `/review`), `message` (the list of field errors + for a request-schema failure; the parameter path on `/review` and + `/citecheck`) and `errors`; `reset_in_seconds` and `retry_after` on a + 429; `LenzQuotaExceededError.credit_balance` on a citation-check 402. - `map_response_to_error` takes an optional `endpoint=(method, path)`, which - the client passes: a 422's original `code` and wording depend on it. + the client passes: an error's original `code` and wording depend on it. ### What reads differently -The current shape does not carry a few values, so these attributes read the -server's current values: - -- A failed verification's sentence (`TaskStatus.error`, the message of the - `LenzPipelineError` that `wait` raises) is the current wording, e.g. "No - sources about the claim were found." where 2.x read "Pipeline stopped at: - research_empty". `failure_reason`, `failure_class` and `retryable` are - unchanged. -- Some error sentences are worded differently ("claim is required." for - "Text is required."). -- An error's `code` is set on every error, including those where 2.x read - `""` (400s such as `malformed_body` and `verification_not_ready` from - `ask.send`, 401, 404, 405, 409 and some 422s); a 422 may list `errors` - where 2.x listed none; a failed verification may carry a `hint` where 2.x - read `""`. -- `verifications.get` on a run that failed (409 `verification_failed`): the - `LenzPipelineError`'s `hint` and `fix`, and the CLI's message, carry the - run's own advice where it has one (e.g. "Send one factual claim, ...") where - 2.x sometimes read "Resubmit to POST /verify if retryable is true. ...". -- `ExtractedClaims.status` reads `ready` when claims came back beside input - that held none (2.x: `not_a_claim`). -- A review's quick-check row whose passage held several claims has no `hint`. -- Reviews stored before the API's current shape: a quick-check row or a - `ReviewFailure` that 2.x read with `failure` `None` may read a failure block, - and a deep check's `modified_at` is computed from its completion time - (set only when it completed on a later UTC calendar day) rather than read - as stored. -- A `review.*` / `citecheck.*` webhook event's `task_id` reads the review or - check id (the payload no longer carries the delivery id; deduplicate on - `event_id`). -- `TaskAccepted.chain_id` (an internal id, never pollable) reads `""`. +The API now words or sends a few things differently, which no client can +rebuild: + +- A failed check's `error` (and the `LenzPipelineError` message built from + it) reads "Pipeline stopped at: " or its fixed sentence, as a running + check's failure did; a failure read back from storage said "Pipeline + stopped: ." in 2.x. +- The 409 `verification_failed` from `verifications.get` carries the run's + own `hint` (and so `fix`, and the CLI's message), where 2.x sometimes + carried a generic one; a failed poll read back from storage can carry a + `hint` 2.x left out. +- Some other hints and 4xx messages are worded anew (the `message` / `cause` + of a blank input or an unparseable body, a failed review's hint). +- Reviews: a review row stored without a failure block (a quick-check row or + a `ReviewFailure`) reads one, where 2.x read `failure` `None`; a deep + check's `modified_at` is computed from its completion time by the 2.x rule + instead of read as stored; a quick-check row whose passage held several + claims has no `hint`. +- An extraction the API first read as not a claim and then found one in says + `status == "ready"` (2.x: `"not_a_claim"`). +- `verify`'s receipt has no `chain_id` (`TaskAccepted.chain_id` reads `""`); + the `task_id` of a `review.*` / `citecheck.*` webhook event is the review / + citation-check id (deduplicate on `event_id`); a repeated `verify` answered + from the first one is a 202 (the SDK returns the same receipt either way). ### Migration diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 7ea1028..1f74ac2 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -515,100 +515,139 @@ def _parse_body(raw: bytes | str | None) -> dict[str, Any]: return parsed if isinstance(parsed, dict) else {} -# ── The original 422 shapes, read from a newer-shape body ─────────────── +# ── The original error bodies, read from a newer-shape body ───────────── # -# The API's newer response shape gives every 422 one envelope: ``detail`` is a -# sentence, ``code`` is set and ``errors`` lists the fields. The original shape -# differed by endpoint, and a few codes were spelled differently. The -# exception's attributes keep their original values; ``exc.body`` is the body -# as sent. Each rule recognises only a newer-shape body, so an original-shape -# body reads exactly as before. - -#: /review and /citecheck (submit and read): one envelope in both shapes, -#: with the original ``code`` / ``detail`` wording below. +# The API's newer response shape gives every error one envelope: ``detail`` is +# a sentence, ``code`` is always set, a 422 lists its fields in ``errors``, and +# waits and links have one name each. The original shape differed by +# endpoint. ``_original_error`` rebuilds the original body by endpoint (the +# same rules as the Node SDK), so every attribute of the exception keeps its +# 2.x value; ``exc.body`` is the body as sent. Each rule matches only what the +# newer shape alone sends, so an original-shape body comes back unchanged. + +#: /review and /citecheck (submit and read) kept their own error envelope. _REVIEW_FAMILY = re.compile(r"^/(?:review|reviews/[^/]+|citecheck|citechecks/[^/]+)$") -#: The codes a /review 422 renamed (the original said ``validation_error``). -_REVIEW_RENAMED_422 = ("blank_input", "unsupported_language") - -def _items(parsed: dict[str, Any]) -> list[dict[str, Any]] | None: - errors = parsed.get("errors") - if not isinstance(errors, list) or not all(isinstance(i, dict) for i in errors): - return None - return errors - - -def _payload_loc(item: dict[str, Any]) -> bool: - """A field error from request-body validation (``body.payload...``).""" - loc = item.get("loc") - return isinstance(loc, list) and len(loc) >= 2 and loc[0] == "body" and loc[1] == "payload" - - -def _param_loc(item: dict[str, Any]) -> bool: - """A field error from query or path parameter validation.""" - loc = item.get("loc") - return isinstance(loc, list) and len(loc) >= 2 and loc[0] in ("query", "path") - - -def _review_family_original(parsed: dict[str, Any]) -> dict[str, Any]: - code, detail, items = parsed.get("code"), parsed.get("detail"), _items(parsed) - view = dict(parsed) - if code in _REVIEW_RENAMED_422: - view["code"] = "validation_error" - if code == "unsupported_language" and isinstance(detail, str) and not detail.startswith("language: "): - view["detail"] = f"language: {detail}" - if items: - first = items[0] - if _payload_loc(first): - # The original named the body parameter: ``payload.text: ...``. - loc = ".".join(str(p) for p in first["loc"] if p != "body") - view["detail"] = f"{loc}: {first.get('msg')}" - out = [] - for item in items: - item = {k: v for k, v in item.items() if k in ("loc", "msg")} - if view.get("detail") != detail and item.get("msg") == detail: - item["msg"] = view["detail"] - out.append(item) - view["errors"] = out - elif code == "idempotency_body_mismatch" and "errors" not in parsed: - view["errors"] = [{"loc": ["header"], "msg": detail}] - return view - - -def _original_422(parsed: dict[str, Any], method: str, path: str) -> dict[str, Any]: - """``parsed`` as the original response shape read, where the newer body - lets that be worked out; else ``parsed`` itself.""" - if not isinstance(parsed.get("detail"), str): - return parsed # the original shape's list ``detail``, or no detail +#: Codes the newer shape sends where the original error carried no ``code`` +#: (outside /review and /citecheck, which always sent one). +_CODELESS = frozenset( + { + "not_authenticated", + "not_found", + "idempotency_body_mismatch", + "idempotency_conflict", + "malformed_body", + "method_not_allowed", + "validation_error", + "blank_input", + "unsupported_language", + "too_many_items", + } +) + + +def _rename(o: dict[str, Any], old: str, new: str) -> None: + """``old`` renamed to ``new`` when only ``old`` is there (in place).""" + if old in o and new not in o: + o[new] = o.pop(old) + + +def _original_wait_and_link(o: dict[str, Any], status: int) -> None: + """A wait and a docs link under their original names.""" + code = o.get("code") + if status == 429 and code == "extract_daily_limit": + _rename(o, "retry_after", "reset_in_seconds") + if status == 429 and code in ("review_in_flight", "citecheck_in_flight"): + _rename(o, "retry_after", "retry_after_seconds") + if status in (402, 429, 503): + _rename(o, "docs_url", "doc_url") + + +def _original_error(status: int, parsed: dict[str, Any], method: str, path: str) -> dict[str, Any]: + """``parsed`` as the original response shape sent it, as far as the newer + body and the request's endpoint tell.""" + out = dict(parsed) + code = out["code"] if isinstance(out.get("code"), str) else "" + errors = out["errors"] if isinstance(out.get("errors"), list) else None path = path.split("?", 1)[0] if _REVIEW_FAMILY.match(path): - return _review_family_original(parsed) - code, items = parsed.get("code"), _items(parsed) - if not items: - return parsed + # A missing or unknown credential is refused before the endpoint runs. + if code == "not_authenticated": + del out["code"] + if status == 422: + detail = out.get("detail") + if method.upper() == "POST" and path == "/review" and isinstance(detail, str): + if code in ("blank_input", "unsupported_language"): + out["code"] = "validation_error" + if code == "unsupported_language" and not detail.startswith("language: "): + out["detail"] = f"language: {detail}" + if errors is not None: + first: dict[str, Any] = next((e for e in errors if isinstance(e, dict)), {}) + loc, msg = first.get("loc"), first.get("msg") + if isinstance(loc, list) and len(loc) > 1 and loc[1] == "payload" and isinstance(msg, str): + # The original named the body parameter: ``payload.text: ...``. + out["detail"] = ".".join(str(p) for p in loc[1:]) + f": {msg}" + items: list[Any] = [] + for item in errors: + if not isinstance(item, dict): + items.append(item) + continue + o: dict[str, Any] = {} + if "loc" in item: + o["loc"] = item["loc"] + if "msg" in item: + renamed = item["msg"] == detail and out.get("detail") != detail + o["msg"] = out["detail"] if renamed else item["msg"] + items.append(o) + out["errors"] = items + elif code == "idempotency_body_mismatch": + out["errors"] = [{"loc": ["header"], "msg": out.get("detail")}] + if ( + status == 402 + and method.upper() == "POST" + and path == "/citecheck" + and "credits_remaining" not in out + and isinstance(out.get("remaining"), int) + ): + # One credit per citation: the pool equals ``remaining``. + out["credits_remaining"] = out["remaining"] + _original_wait_and_link(out, status) + return out + if status == 422 and code == "blank_input" and path == "/assess": + loc = errors[0].get("loc") if errors and isinstance(errors[0], dict) else None + if isinstance(loc, list) and "claims" in loc: + # A blank item in ``claims``: the original said ``blank_item``. + out["code"] = "blank_item" + out.pop("errors", None) + return out if ( - method.upper() == "POST" - and path == "/assess" - and code == "blank_input" - and isinstance(items[0].get("loc"), list) - and items[0]["loc"][:2] == ["body", "claims"] - and len(items[0]["loc"]) == 3 + status == 422 + and code == "validation_error" + and errors + and all(isinstance(e, dict) and isinstance(e.get("type"), str) and e["type"] != code for e in errors) ): - # A blank item in ``claims``: the original said ``blank_item`` and - # listed no field errors. - return {k: v for k, v in {**parsed, "code": "blank_item"}.items() if k != "errors"} - if code == "validation_error" and all(_payload_loc(i) or _param_loc(i) for i in items): - if any(i.get("type") in (None, code) for i in items): - return parsed # the endpoint's own check, not request-schema validation # Request-schema validation: the original ``detail`` was the list of # field errors itself, each ``{type, loc, msg, ...}``, with no ``code``. listed = [] - for item in items: + for item in errors: ordered = {k: item[k] for k in ("type", "loc", "msg") if k in item} ordered.update({k: v for k, v in item.items() if k not in ordered}) listed.append(ordered) - return {k: v for k, v in {**parsed, "detail": listed}.items() if k not in ("code", "errors")} - return parsed + original: dict[str, Any] = {"detail": listed} + for key, value in out.items(): + if key not in ("detail", "code", "errors"): + original["doc_url" if key == "docs_url" else key] = value + return original + # /assess sent ``too_many_items``; /ask sent no code for an unfinished + # verification (``GET /verifications/{id}`` still sends ``verification_not_ready``). + codeless = (code in _CODELESS and not (code == "too_many_items" and path == "/assess")) or ( + code == "verification_not_ready" and path.startswith("/ask/") + ) + if codeless: + del out["code"] + out.pop("errors", None) + _original_wait_and_link(out, status) + return out def map_response_to_error( @@ -623,15 +662,15 @@ def map_response_to_error( Returns an *instance* (not raised) so callers can decide whether to raise, log, or surface. Keep this pure — no I/O. - ``endpoint`` is the request's ``(method, path)``: with it, a 422 in the - API's newer response shape gives the exception the same ``code``, - ``message`` and ``errors`` the original shape gave (``exc.body`` is - always the body as sent). + ``endpoint`` is the request's ``(method, path)``: with it, an error in + the API's newer response shape gives the exception the same attributes + the original shape gave (``code``, ``message``, ``errors``, waits); + ``exc.body`` is always the body as sent. """ raw = _parse_body(body) parsed = raw - if status_code == 422 and endpoint is not None: - parsed = _original_422(raw, *endpoint) + if endpoint is not None: + parsed = _original_error(status_code, raw, *endpoint) headers = headers or {} request_id = headers.get("X-Request-ID") or headers.get("x-request-id") or "" diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 7dfe457..f8194ef 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -915,6 +915,26 @@ def items(self) -> ItemsView[str, Any]: return self.model_dump().items() +#: A failed poll's original ``error`` sentence, for the codes that had a fixed +#: one. +_STATUS_ERRORS = { + "cancelled": "Cancelled.", + "task_stuck": "The task was never completed and has been marked failed.", + "task_error": "Pipeline failed.", + "not_a_claim": "Not a verifiable claim.", +} + + +def _original_status_error(code: Any, detail: Any) -> Any: + """A failed poll's original ``error``: the code's fixed sentence, else + "Pipeline stopped at: " (a running check's form; one read back from + storage said "Pipeline stopped: ."). The newer shape's sentence when + there is no code.""" + if isinstance(code, str) and code: + return _STATUS_ERRORS.get(code, f"Pipeline stopped at: {code}") + return detail + + class TaskStatus(_Lax): """Returned by ``GET /verify/status/{task_id}``.""" @@ -967,9 +987,10 @@ def _read_newer_shape(cls, data: Any) -> Any: if not _is_newer(data, "failure", "failure_reason") or not isinstance(data["failure"], dict): return data failure = data["failure"] + reason = _old_code(failure.get("code"), "not_a_claim") values = { - "error": failure.get("detail"), - "failure_reason": _old_code(failure.get("code"), "not_a_claim"), + "error": _original_status_error(reason, failure.get("detail")), + "failure_reason": reason, "failure_class": failure.get("failure_class"), "retryable": failure.get("retryable"), "docs_url": failure.get("docs_url"), diff --git a/tests/parity_observe.py b/tests/parity_observe.py index e60c4c6..4852e3c 100644 --- a/tests/parity_observe.py +++ b/tests/parity_observe.py @@ -149,6 +149,8 @@ def endpoint_for(name: str) -> tuple[str, str]: return ("POST", "/assess") if area == "extract": return ("POST", "/extract") + if area == "errors" and case.startswith("ask_"): + return ("POST", "/ask/v1") return ("POST", "/verify") diff --git a/tests/test_current_api_version.py b/tests/test_current_api_version.py index 657f25a..c632d43 100644 --- a/tests/test_current_api_version.py +++ b/tests/test_current_api_version.py @@ -241,3 +241,45 @@ def test_a_blank_verify_webhook_url_is_left_out(client: Lenz, blank: Any) -> Non body = _sent(batch) assert "webhook_url" not in body assert "webhook_url" not in body["claims"][0] + + +@pytest.mark.parametrize( + ("fixture", "method", "path", "code"), + [ + ("verify__status_unknown_404.json", "GET", "/verify/status/t1", ""), + ("verify__unauthenticated_401.json", "POST", "/verify", ""), + ("review__401_no_credentials.json", "POST", "/review", ""), + ("errors__ask_not_completed.json", "POST", "/ask/v1", ""), + ("verify__idempotency_conflict_409.json", "POST", "/verify", ""), + ("verify__verification_not_ready_409.json", "GET", "/verifications/t1", "verification_not_ready"), + ("review__get_404_not_found.json", "GET", "/reviews/r1", "not_found"), + ], +) +def test_an_error_code_reads_as_it_did_in_2x(fixture: str, method: str, path: str, code: str) -> None: + """The current shape names a ``code`` on every error; where the 2.x + error carried none, the exception's ``code`` stays ``""``.""" + from lenz_io.errors import map_response_to_error + + response = _canonical(fixture) + err = map_response_to_error(response["status"], json.dumps(response["body"]), endpoint=(method, path)) + assert err.code == code == (load("legacy", fixture)["body"].get("code") or "") + assert err.body == response["body"] + + +@pytest.mark.parametrize( + ("code", "sentence"), + [ + ("research_empty", "Pipeline stopped at: research_empty"), + ("cancelled", "Cancelled."), + ("task_stuck", "The task was never completed and has been marked failed."), + ("no_checkable_claim", "Not a verifiable claim."), + ], +) +def test_a_failed_poll_reads_its_2x_sentence(code: str, sentence: str) -> None: + from lenz_io.models import TaskStatus + + status = TaskStatus.model_validate( + {"status": "failed", "task_id": "t", "failure": {"code": code, "detail": "Current wording."}} + ) + assert status.error == sentence + assert status.failure is not None and status.failure.detail == "Current wording." diff --git a/tests/test_parity.py b/tests/test_parity.py index a2abb2c..45315a1 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -68,10 +68,6 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, return "the server words its `detail` sentence differently" if rest in _MESSAGE_PATHS and "detail" not in lb and isinstance(cb.get("detail"), str): return "the newer shape sends a `detail` sentence where the original sent none" - if rest == "code" and "code" not in lb and "code" in cb: - return "the newer shape names a `code` where the original sent none" - if rest.startswith("errors") and not _validation_items(lb) and _validation_items(cb): - return "the newer shape lists the field errors where the original listed none" if rest in ("hint", "fix", "friendly_text", "payload_json.error.fix") and lb.get("hint") != cb.get("hint"): return "the server words the hint differently" if head == "wait" or path in ("dump.error", "render"): @@ -79,11 +75,14 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, if ( name.startswith("verify__") and isinstance(sentence, str) - and sentence != _failure(cb).get("detail") + and sentence.startswith("Pipeline stopped: ") and "failure" in cb ): if path in ("dump.error", "render") or rest in _MESSAGE_PATHS: - return "the failed run's sentence: the newer shape's `failure.detail` is worded differently" + return ( + "a failure read back from storage said 'Pipeline stopped: .'; " + "the sentence is rebuilt in a running check's form" + ) if path in ("dump.hint", "wait.hint") and lb.get("hint", "") != _failure(cb).get("hint", ""): return "the newer shape carries a hint the original did not" option_claim = path.startswith("wait.payload.claims[") and path.endswith("].claim") @@ -100,6 +99,9 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, #: Single gaps no rule covers, each with its reason. KNOWN_GAPS: dict[tuple[str, str], str] = { + ("review__delete_not_a_route.json", "error.code"): ( + "a route no SDK method calls: the original answered it without a JSON body" + ), ("review__get_assessment_rows_full_fields.json", "dump.claims[0].assessment.hint"): ( "the newer shape sends no hint on a completed quick check with other claims found" ), diff --git a/tests/test_read_both_shapes.py b/tests/test_read_both_shapes.py index a8741bb..f26b9e4 100644 --- a/tests/test_read_both_shapes.py +++ b/tests/test_read_both_shapes.py @@ -164,7 +164,10 @@ def test_failed_status_failure_block_from_both_shapes(name): assert st.failure.retryable is canonical.retryable assert st.failure.detail assert canonical.failure_reason == legacy.failure_reason - assert canonical.error == canonical.failure.detail + # ``error`` keeps the original sentence, rebuilt from the code (a failure + # read back from storage said "Pipeline stopped: ." instead). + if not legacy.error.startswith("Pipeline stopped: "): + assert canonical.error == legacy.error def test_needs_input_options_from_both_shapes(): From 1d3b3aa0c6641b7e7890c5ceaee11a137c47b80a Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Oct 2026 22:57:03 +0300 Subject: [PATCH 10/67] fix: 2.x empty code for the fallback codes, a batch item's language error names its item - `internal_error` and `invalid_request` read as "" on the exception, as in 2.x, where the error carried no code. - A verify_batch item with an unsupported language: the message is "claims[]." again, rebuilt from the error's location. - Changelog: the headline names the one exception (raw bodies show the newer shape), and the failed-poll entry says task_error / task_stuck read one fixed 2.x sentence where 2.x had several. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 21 ++++++++++++++------- src/lenz_io/errors.py | 10 ++++++++++ tests/test_current_api_version.py | 26 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 706a193..d1a0a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,10 @@ All notable changes to this SDK are documented here. Format follows ## [Unreleased] -Major release (3.0.0). Code that reads the SDK's attributes keeps working -unchanged. Code that reads the raw response dict (`model_dump()`, -`exc.body`, a webhook event's `raw`, the CLI's `--json` output) sees the -API's current response shape: read the migration note below. +Major release (3.0.0). Every attribute, exception and CLI rendering reads +what 2.x read, with one exception: the raw bodies (`exc.body`, +`model_dump()`, a webhook event's `raw`, the CLI's `--json` output) show the +API's newer response shape as sent. See the migration note below. ### Changed @@ -46,11 +46,13 @@ API's current response shape: read the migration note below. - Errors: every class and attribute as 2.x set it. `code` is `""` where the 2.x error carried none (the API now sends one on every error: `not_authenticated`, `not_found`, `validation_error`, `malformed_body`, - `verification_not_ready` from `ask.send`, ...), per endpoint; a 422 keeps + `invalid_request`, `internal_error`, `verification_not_ready` from + `ask.send`, ...), per endpoint; a 422 keeps its 2.x `code` (`blank_item` for a blank `/assess` item, `validation_error` on `/review`), `message` (the list of field errors for a request-schema failure; the parameter path on `/review` and - `/citecheck`) and `errors`; `reset_in_seconds` and `retry_after` on a + `/citecheck`; `claims[].` before a `verify_batch` item's language + error) and `errors`; `reset_in_seconds` and `retry_after` on a 429; `LenzQuotaExceededError.credit_balance` on a citation-check 402. - `map_response_to_error` takes an optional `endpoint=(method, path)`, which the client passes: an error's original `code` and wording depend on it. @@ -63,7 +65,12 @@ rebuild: - A failed check's `error` (and the `LenzPipelineError` message built from it) reads "Pipeline stopped at: " or its fixed sentence, as a running check's failure did; a failure read back from storage said "Pipeline - stopped: ." in 2.x. + stopped: ." in 2.x. A `task_error` reads "Pipeline failed." and a + `task_stuck` "The task was never completed and has been marked failed.", + where 2.x said one of several sentences for each (e.g. "Unexpected + result.", "Unexpected pipeline step: ", "We hit a snag finalizing + your result. Please try submitting again.", "The task was never picked up + and has been marked failed."). - The 409 `verification_failed` from `verifications.get` carries the run's own `hint` (and so `fix`, and the CLI's message), where 2.x sometimes carried a generic one; a failed poll read back from storage can carry a diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 1f74ac2..5016f84 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -542,6 +542,9 @@ def _parse_body(raw: bytes | str | None) -> dict[str, Any]: "blank_input", "unsupported_language", "too_many_items", + # The fallback codes: any 4xx without its own, and a 500. + "invalid_request", + "internal_error", } ) @@ -620,6 +623,13 @@ def _original_error(status: int, parsed: dict[str, Any], method: str, path: str) out["code"] = "blank_item" out.pop("errors", None) return out + if status == 422 and code == "unsupported_language" and path == "/verify/batch" and errors: + loc = errors[0].get("loc") if isinstance(errors[0], dict) else None + detail = out.get("detail") + if isinstance(loc, list) and len(loc) > 2 and loc[1] == "claims" and isinstance(loc[2], int): + if isinstance(detail, str) and not detail.startswith("claims["): + # The original named the item: ``claims[1].Unsupported language ...``. + out["detail"] = f"claims[{loc[2]}].{detail}" if ( status == 422 and code == "validation_error" diff --git a/tests/test_current_api_version.py b/tests/test_current_api_version.py index c632d43..bd6a2dd 100644 --- a/tests/test_current_api_version.py +++ b/tests/test_current_api_version.py @@ -283,3 +283,29 @@ def test_a_failed_poll_reads_its_2x_sentence(code: str, sentence: str) -> None: ) assert status.error == sentence assert status.failure is not None and status.failure.detail == "Current wording." + + +@pytest.mark.parametrize( + ("status", "code"), [(500, "internal_error"), (400, "invalid_request"), (422, "invalid_request")] +) +def test_the_fallback_codes_read_as_none(status: int, code: str) -> None: + from lenz_io.errors import map_response_to_error + + err = map_response_to_error(status, json.dumps({"detail": "x", "code": code}), endpoint=("POST", "/verify")) + assert err.code == "" + assert err.body == {"detail": "x", "code": code} + + +def test_a_batch_item_language_error_names_its_item() -> None: + from lenz_io.errors import map_response_to_error + + sentence = "Unsupported language 'xx'. Supported: en." + body = { + "detail": sentence, + "code": "unsupported_language", + "errors": [{"loc": ["body", "claims", 1, "language"], "msg": sentence, "type": "unsupported_language"}], + } + err = map_response_to_error(422, json.dumps(body), endpoint=("POST", "/verify/batch")) + assert err.message == f"claims[1].{sentence}" + assert err.code == "" + assert err.errors == [] From e59c1cdd6dd889815c9105e6414d1a8eda54b41c Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 12:37:38 +0300 Subject: [PATCH 11/67] docs: 3.0 keeps the 2.x names, marked deprecated with their replacements 3.0 reads only the 2026-10-11 response shape for its own calls (webhooks of both shapes are still parsed). The current names are the primary interface in the README, examples, CLI and docstrings; every 2.x alias stays with its 2.x value, documented as deprecated, listed in the changelog. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 71 +++- README.md | 39 +- examples/core/quickstart.py | 14 +- examples/core/verify_batch.py | 8 +- examples/core/verify_llm_output.py | 24 +- pyproject.toml | 3 + src/lenz_io/cli/citecheck.py | 2 +- src/lenz_io/cli/render.py | 75 ++-- src/lenz_io/cli/review.py | 12 +- src/lenz_io/cli/verify.py | 6 +- src/lenz_io/client.py | 22 +- src/lenz_io/errors.py | 60 ++- src/lenz_io/models.py | 344 +++++++++--------- .../canonical/extract__replay_later_200.json | 17 +- tests/test_cli.py | 123 +++---- tests/test_current_api_version.py | 18 +- tests/test_parity.py | 29 +- tests/test_read_both_shapes.py | 199 +++++----- tests/test_usage_models.py | 15 - uv.lock | 3 + 20 files changed, 564 insertions(+), 520 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c70ee42..7e163b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,24 @@ All notable changes to this SDK are documented here. Format follows Major release (3.0.0). Every attribute, exception and CLI rendering reads what 2.x read, with one exception: the raw bodies (`exc.body`, `model_dump()`, a webhook event's `raw`, the CLI's `--json` output) show the -API's newer response shape as sent. See the migration note below. +API's current response shape as sent. Every 2.x name is kept as a deprecated +alias (see Deprecated); see the migration note below. + +### Breaking + +- **3.0 reads only the API's `2026-10-11` response shape for its own calls** + (lenz.io serves it from 2026-10-11). Run against a server that does not + serve that version, it does not read the 2.x (`2026-05-13`) response + shape any more: stay on 2.21 until the API serves `2026-10-11`. The SDK no + longer detects or fills in a 2.x-shaped response body, a 2.x-shaped error + body, or a `/me/usage` body from before the credit pool. +- **Webhooks of both shapes are still parsed** (`parse_webhook`, + `LenzWebhooks.parse` and the event models): work submitted by an older + client on the same account is delivered in the 2.x shape. ### Changed -- **The SDK asks for the API's current response shape.** Every request sends +- **The SDK asks for the API's current response shape, and reads only that.** Every request sends `X-Lenz-API-Version: 2026-10-11` (`lenz_io.API_VERSION`; 2.x sent `2026-05-13`). In that shape each field, status and error code has one name across every endpoint. @@ -57,6 +70,55 @@ API's newer response shape as sent. See the migration note below. - `map_response_to_error` takes an optional `endpoint=(method, path)`, which the client passes: an error's original `code` and wording depend on it. +### Deprecated + +Kept in 3.x with their 2.x values, so 2.x code runs unchanged; they will be +removed in a future major release. Move to the newer names when convenient. +Reading them emits no new warning (the ones that already warned still do); +the deprecated properties carry a PEP 702 `@deprecated` marker, so editors +strike them through and type checkers report them. `typing_extensions>=4.5` is +now a declared dependency (pydantic already installs it). + +| 2.x name | Use instead | +|---|---| +| `ExtractedClaims.claim` | `claims[0].claim` | +| `ExtractedClaims.identified_claims` | `claims` (each `.claim`) | +| `ExtractedClaims.locations` | `claims` (each `.positions`) | +| `ExtractedClaims.candidate_claims`, `AssessClaim.candidate_claims`, `AssessResponse.candidate_claims`, `TaskStatus.candidates`, `TaskStatus.similar_claims` | none: always empty | +| `AssessClaim.verdict == "Error"` (with `confidence == "low"`) | `status == "failed"` | +| `AssessClaim.error_code` | `failure.code` (`no_checkable_claim` where it reads `no_claim`) | +| `AssessClaim.hint` | `failure.hint`; none for the sentence on a completed row that found other claims | +| `AssessClaim.identified_claims`, `ReviewAssessment.identified_claims` | `more_claims` | +| `ReviewAssessment.error_code`, `ReviewAssessment.hint` | `failure.code`, `failure.hint` | +| `AssessResponse.error`, `AssessResponse.error_code` | `status` and `failure` | +| `CandidateClaim.text` | `claim` | +| `TaskAccepted.claim_text`, `BatchItemResult.claim_text` | `claim` | +| `TaskAccepted.chain_id` | none: no longer sent, reads `""` | +| `TaskStatus.error` | `failure.detail` | +| `TaskStatus.failure_reason` | `failure.code` (`no_checkable_claim` where it reads `not_a_claim`) | +| `TaskStatus.failure_class`, `.retryable`, `.docs_url` | `failure.failure_class`, `.retryable`, `.docs_url` | +| `TaskStatus.hint` on a failed run | `failure.hint` | +| `TaskStatus.failure_detail` | none: always `""` | +| `FailureBlock.failure_reason` | `code` | +| `Verification.modified_at`, `VerificationListItem.modified_at`, `ReviewVerification.modified_at` | `completed_at` | +| `ReviewSummary.claim_limit_reached` | `claim_limit_exceeded` (some claims were left out; `reached` also held at exactly the limit) | +| `ReviewSummary.citation_limit_reached`, `CitecheckSummary.citation_limit_reached` | `citation_limit_exceeded` | +| `Usage.verify`, `Usage.ask`, `Usage.assess` | `credits.remaining // costs[]` (and `credits.total`, `credits.extra`) | +| `Usage.quota_resets_at` | `credits.resets_at` | +| `UsageCredits.bonus` | `extra` (warns) | +| `UsageCapacity.credits` | `UsageCapacity.bonus` (warns); the block itself is deprecated, see above | +| `LenzQuotaExceededError.credits_remaining` | `remaining` (warns); `credit_balance` is the credit pool | +| `Progress` mapping access (`p["step"]`, `p.get`, `in`, `keys()`, `values()`, `items()`) | attributes (`p.step`) | +| `ASSESS_LIST_TIMEOUT` | `ASSESS_TIMEOUT` | + +`LenzQuotaExceededError.credits_remaining` and the `Progress` mapping access +were announced for removal in 3.0: they are kept instead, with their warnings +(where they had one), and will now be removed in a future major release, like +the rest of this list. + +The status value `not_a_claim` on `/extract` reads where the API now sends +`no_checkable_claim`; there is no other spelling to read it by. + ### What reads differently The API now words or sends a few things differently, which no client can @@ -100,7 +162,8 @@ rebuild: upgrade it to 2.21 or later (which reads both shapes) before the service that SENDS requests moves to 3.0. Work submitted with 2.x keeps sending the original shape. -- Code that reads attributes: nothing to do. +- Code that reads attributes: nothing to do. The deprecated names above keep + working; the SDK needs an API that serves `2026-10-11` (see Breaking). - Code that reads the raw dict: the keys are the current shape's. A model's `model_dump()` holds what the server sent plus the 2.x fields filled in from it; `exc.body` and `event.raw` hold the body as sent (`failure` blocks, @@ -113,8 +176,6 @@ rebuild: webhook. In the current shape the API reads a missing `webhook_url` as the key's default and `""` as no webhook on every endpoint. ---- - ## [2.21.0] - 2026-10-09 Minor release. Existing code keeps working unchanged; nothing to do on diff --git a/README.md b/README.md index e0520f4..e2dee87 100644 --- a/README.md +++ b/README.md @@ -220,8 +220,8 @@ edited = "".join(chars) **Waiting.** `review_and_wait` polls on the review's own `poll_after_seconds`. Pass `on_update=` to see the quick verdicts as soon as they are in and each deep check as it lands; without it the helper is silent. -It raises `ReviewFailed` when the review fails (`error_code` and `hint` say -why; `review.failure.code` is the cause in the newer spelling) and `ReviewTimeout` after `timeout` seconds (600 by default); the review +It raises `ReviewFailed` when the review fails (`review.failure.code` and +`review.failure.hint` say why; the exception's `error_code` keeps its 2.x spelling) and `ReviewTimeout` after `timeout` seconds (600 by default); the review keeps running, and the error carries its `review_id` and the last body read. To submit without waiting, `client.review(draft)` returns a `review_id`; read it with `client.get_review(review_id)`, or only the issues with @@ -471,12 +471,15 @@ Every claim-shaped response shares these fields at top level: | `confidence` | `str` | Categorical: `"high"` \| `"medium"` \| `"low"`. | | `lenz_score` | `int \| None` | Integer 1–10 (deep verdicts and list endpoints; `assess` omits it). | -### Field names: both response shapes +### Field names: the current names, and the deprecated 2.x ones -The API is adding a newer, dated response shape that gives each field one -name across every endpoint. This SDK still asks for the original shape, and -its models read either one. The newer names are attributes already; the -older ones keep working, with the meaning they always had: +The API's current response shape gives each field one name across every +endpoint. Since 3.0 the SDK asks for it (`X-Lenz-API-Version: 2026-10-11`) and +reads only that shape from its own calls (webhooks of both shapes are still +parsed). The current names are attributes already. The 2.x names are +**deprecated** and still work, with the value they had in 2.x, so 2.x code +runs unchanged; they will be removed in a future major release. Move to the +current names when convenient: | Read this | Instead of (deprecated, still works) | |---|---| @@ -484,17 +487,18 @@ older ones keep working, with the meaning they always had: | `AssessClaim.status` (`"completed"` / `"failed"`) and `.failure` | `verdict == "Error"`, `error_code`, `hint` | | `AssessClaim.more_claims`, `ReviewAssessment.more_claims` | `identified_claims` | | `AssessResponse.status` and `.failure` | `error`, `error_code` | -| `TaskStatus.failure` (`code`, `detail`, `hint`, `failure_class`, `retryable`, `docs_url`) | `error`, `failure_reason` and the flat fields | +| `TaskStatus.failure` (`code`, `detail`, `hint`, `failure_class`, `retryable`, `docs_url`) | `error`, `failure_reason`, `failure_detail` and the flat fields | | `TaskAccepted.claim`, `BatchItemResult.claim`, `CandidateClaim.claim` | `claim_text`, `text` | | `Verification.completed_at` | `modified_at` | | `ReviewSummary.claim_limit_exceeded`, `citation_limit_exceeded` | `claim_limit_reached`, `citation_limit_reached` | | `FailureBlock.code`, `.detail` | `failure_reason` | | `Usage.credits` and `Usage.costs` | the `verify` / `ask` / `assess` blocks, `quota_resets_at` | -"Nothing checkable" is `no_checkable_claim` in the newer names; the older -fields keep their own spelling (`not_a_claim`, `no_claim`). The newer names -are read-only properties, so an original-shape response parses, dumps and -compares exactly as before. +"Nothing checkable" is `no_checkable_claim` in the current names; the 2.x +fields keep their own spelling (`not_a_claim`, `no_claim`). The current names +are read-only properties, so the 2.x fields parse, dump and compare as they +did. The full list, with the aliases that have no replacement, is in the +[changelog](CHANGELOG.md). ### A suggested rewrite (`suggested_rewrite`) @@ -658,14 +662,15 @@ u = client.usage() print(u.credits.remaining, "credits") # the balance — the authoritative number print(u.costs["verify"], "credits per verification") # the price list print(u.cost_options["verify"]["depth"]["low"], "at depth low") # 5 — half price -print(u.verify.remaining, "verifications left") # a projection of that balance +print(u.credits.remaining // u.costs["verify"], "verifications left") # that balance in verifications print(u.credits.extra, "of them non-expiring") # grants + top-ups ``` -The `verify` / `ask` / `assess` blocks are **projections** of the one balance -into each capability's unit — how many of those calls the remaining credits -would buy — not separate allowances. Spending on any one of them moves all of -them. +The `verify` / `ask` / `assess` blocks on `Usage` are deprecated (kept, with +their 2.x values) **projections** of the one balance into each capability's +unit — how many of those calls the remaining credits would buy — not separate +allowances. Spending on any one of them moves all of them. Derive the same +number from `credits` and `costs`, as above. `credits.extra` is the non-expiring part of the balance. Its old name, `credits.bonus`, is deprecated: the same number, it emits a diff --git a/examples/core/quickstart.py b/examples/core/quickstart.py index 73e3b2b..ff03abf 100644 --- a/examples/core/quickstart.py +++ b/examples/core/quickstart.py @@ -27,7 +27,7 @@ def main() -> None: # 1. extract — pull verifiable claims out of any text (free) out = client.extract(text="Sharks don't get cancer. The Eiffel Tower is 330m tall.") - claims = out.identified_claims or [out.claim] + claims = [c.claim for c in out.claims] print(f"Extracted {len(claims)} claims:") for c in claims: print(f" - {c}") @@ -37,17 +37,17 @@ def main() -> None: quick = client.assess(claims=claims).claims for c in quick: print(f" {c.verdict:<12} conf={c.confidence:<7} {c.claim}") - if c.verdict == "Error": - # No verdict for this item — error_code says why, hint says what to send next. - print(f" {c.error_code}: {c.hint}") - if c.identified_claims: + if c.failure: + # No verdict for this item — failure.code says why, failure.hint says what to send next. + print(f" {c.failure.code}: {c.failure.hint}") + if c.more_claims: # A compound item: only its main claim was assessed. - print(f" also found (not assessed): {c.identified_claims}") + print(f" also found (not assessed): {c.more_claims}") print() # 3. verify — escalate the low-confidence rows to the full multi-model panel # verify_batch_and_wait takes up to 20 claims a call: the first 20 here - doubtful = [{"claim": c.claim} for c in quick if c.verdict != "Error" and c.confidence == "low"][:20] + doubtful = [{"claim": c.claim} for c in quick if c.status == "completed" and c.confidence == "low"][:20] # Fall back to the demo claim so the walkthrough always reaches steps 3 # and 4 even when every row came back confident. doubtful = doubtful or [{"claim": "Sharks don't get cancer"}] diff --git a/examples/core/verify_batch.py b/examples/core/verify_batch.py index 918a6e8..7f89a0d 100644 --- a/examples/core/verify_batch.py +++ b/examples/core/verify_batch.py @@ -31,14 +31,14 @@ def main() -> None: for r in results: if r.status == "completed" and r.verification is not None: - print(f"[completed] {r.claim_text} → {r.verification.verdict} ({r.verification.lenz_score})") + print(f"[completed] {r.claim} → {r.verification.verdict} ({r.verification.lenz_score})") elif r.status == "failed": detail = r.status_detail - reason = (detail.error or detail.failure_detail) if detail else "unknown" - print(f"[failed] {r.claim_text} → {reason}") + reason = detail.failure.detail if detail and detail.failure else "unknown" + print(f"[failed] {r.claim} → {reason}") else: # needs_input (resolve with client.select) or timeout (poll later) - print(f"[{r.status}] {r.claim_text}") + print(f"[{r.status}] {r.claim}") if __name__ == "__main__": diff --git a/examples/core/verify_llm_output.py b/examples/core/verify_llm_output.py index 343ff0f..604ca3d 100644 --- a/examples/core/verify_llm_output.py +++ b/examples/core/verify_llm_output.py @@ -30,23 +30,23 @@ def main() -> None: # Step 1: extract — pull the verifiable claims out of the answer (free). out = client.extract(text=LLM_OUTPUT) - claims = out.identified_claims or [out.claim] + claims = [c.claim for c in out.claims] print(f"Extracted {len(claims)} claims.\n") # Step 2: assess — one call per 20 extracted claims (extract finds up to # 100; one assess call takes 20), one row per claim, same order. A row - # with verdict "Error" got no verdict: ``error_code`` says why - # (``upstream_unavailable`` is worth a retry) and ``hint`` says what to - # send next. A compound item is assessed on its main claim and lists the - # rest in ``identified_claims``. + # with ``status == "failed"`` got no verdict: ``failure.code`` says why + # (``upstream_unavailable`` is worth a retry) and ``failure.hint`` says + # what to send next. A compound item is assessed on its main claim and + # lists the rest in ``more_claims``. quick = [row for i in range(0, len(claims), 20) for row in client.assess(claims=claims[i : i + 20]).claims] print(f"Assessed {len(quick)} claims:\n") for c in quick: print(f" {c.verdict:<12} conf={c.confidence:<7} {c.claim}") - if c.verdict == "Error": - print(f" {c.error_code}: {c.hint}") - elif c.identified_claims: - print(f" also found (not assessed): {c.identified_claims}") + if c.failure: + print(f" {c.failure.code}: {c.failure.hint}") + elif c.more_claims: + print(f" also found (not assessed): {c.more_claims}") print() # Step 3: verify — escalate the low-confidence rows to the full pipeline. @@ -54,15 +54,15 @@ def main() -> None: # claim that already has a deep verification surfaces immediately # via ``verification_url`` and you can skip the escalation. # verify_batch_and_wait takes up to 20 claims a call: the first 20 here - doubtful = [{"claim": c.claim} for c in quick if c.verdict != "Error" and c.confidence == "low"][:20] + doubtful = [{"claim": c.claim} for c in quick if c.status == "completed" and c.confidence == "low"][:20] print(f"Escalating {len(doubtful)} low-confidence claims to full verification:\n") results = client.verify_batch_and_wait(claims=doubtful, timeout=180) if doubtful else [] for r in results: v = r.verification if v is None: - print(f"{r.status.upper():<14} {r.claim_text}") + print(f"{r.status.upper():<14} {r.claim}") continue - print(f"{v.verdict.upper():<14} (lenz_score {v.lenz_score}) {r.claim_text}") + print(f"{v.verdict.upper():<14} (lenz_score {v.lenz_score}) {r.claim}") if v.verdict.lower() in ("false", "misleading") and v.sources: print(f" ↳ {v.sources[0].title}") print(f" {v.sources[0].url}") diff --git a/pyproject.toml b/pyproject.toml index 0249ca7..2c011f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,9 @@ dependencies = [ # unknown Field kwarg is swallowed into json_schema_extra with a warning of # its own and the deprecation never fires, which is worse than the bump. "pydantic>=2.7", + # `@deprecated(..., category=None)` marks the deprecated property aliases for + # editors and type checkers without a runtime warning. Pydantic needs it too. + "typing_extensions>=4.5", ] [project.optional-dependencies] diff --git a/src/lenz_io/cli/citecheck.py b/src/lenz_io/cli/citecheck.py index 07e7f22..4f561e6 100644 --- a/src/lenz_io/cli/citecheck.py +++ b/src/lenz_io/cli/citecheck.py @@ -236,7 +236,7 @@ def render_citecheck(out: Output, check: Citecheck) -> None: f"Citation check {escape(check.citecheck_id)}: {outcome} — {_count(check.credits.charged, 'credit')} charged" ) if check.failure is not None: - hint = check.failure.hint or check.failure.failure_reason or "no reason given" + hint = check.failure.hint or check.failure.code or "no reason given" out.console.print(f"[red]Failed:[/red] {escape(hint)}") render_citations(out, check) if check.status == "completed" and not check.citation_issues and not check.citation_failures: diff --git a/src/lenz_io/cli/render.py b/src/lenz_io/cli/render.py index 80e7e05..9fa191f 100644 --- a/src/lenz_io/cli/render.py +++ b/src/lenz_io/cli/render.py @@ -12,7 +12,7 @@ import json import sys from datetime import datetime, timezone -from typing import Any, NoReturn +from typing import Any, NamedTuple, NoReturn from rich.console import Console from rich.markdown import Markdown @@ -26,7 +26,6 @@ Source, TaskStatus, Usage, - UsageCapacity, Verification, ) @@ -109,15 +108,11 @@ def render_extract(out: Output, result: ExtractedClaims) -> None: if out.json_mode: out.emit_json(_model_json(result)) return - # The server splits a claim set across two fields: the primary claim lands - # in ``claim`` and any extras in ``identified_claims``. Neither alone is the - # full list — the primary is usually NOT echoed into ``identified_claims`` — - # so render the union so the primary is never dropped (and the count is - # right). Single-claim input → just ``claim``. - primary = (getattr(result, "claim", "") or "").strip() - claims = [primary] if primary else [] - for c in result.identified_claims or []: - c = (c or "").strip() + # ``claims`` is the whole list, most check-worthy first (one entry for one + # claim); blank and repeated entries are skipped. + claims: list[str] = [] + for found in result.claims: + c = (found.claim or "").strip() if c and c not in claims: claims.append(c) if len(claims) > 1: @@ -151,7 +146,7 @@ def _render_positions(out: Output, result: ExtractedClaims, claim: str, *, inden input, where there is nothing to index) and the passage itself. The passage is the user's own text, so it prints with ``markup=False``. """ - for loc in result.locations or []: + for loc in result.claims: if (loc.claim or "").strip() != claim: continue for pos in loc.positions or []: @@ -173,7 +168,7 @@ def render_assess(out: Output, result: AssessResponse) -> None: color = _VERDICT_COLOR.get(c.verdict, "white") # An Error row (list form) names its cause in place of the confidence, # which is meaningless there. - detail = c.error_code if c.verdict == "Error" and c.error_code else c.confidence + detail = c.error_code if c.status == "failed" and c.error_code else c.confidence out.console.print(f"[{color}]{c.verdict or '?'}[/{color}] ({detail}) — {c.claim}") # The reviewers' notes, as plain text: `markup=False` because the # words are a model's, and a stray "[bold]" in them is not ours. @@ -181,9 +176,9 @@ def render_assess(out: Output, result: AssessResponse) -> None: out.console.print(f" {c.rationale}", markup=False, highlight=False) if c.dissent: out.console.print(f" One reviewer disagreed: {c.dissent}", markup=False, highlight=False) - if c.identified_claims: + if c.more_claims: out.console.print(" [dim]also found:[/dim]") - for other in c.identified_claims: + for other in c.more_claims: out.console.print(f" • {other}") if c.hint: out.console.print(f" [dim]{c.hint}[/dim]") @@ -395,7 +390,7 @@ def render_task_status(out: Output, st: TaskStatus, *, task_id: str = "") -> Non if st.claims: out.console.print("[dim]claims found:[/dim]") for i, claim in enumerate(st.claims, 1): - out.console.print(f" {i}. {claim.text}") + out.console.print(f" {i}. {claim.claim}") ref = task_id or "" # Non-interactive resolution (agents/scripts): `--claim` picks by index # and `--detach` returns the spawned task_id(s) without blocking. Drop @@ -510,7 +505,7 @@ def render_ask(out: Output, reply: Any) -> None: def _capacity_row( out: Output, label: str, - cap: UsageCapacity, + cap: _Projection, cost: int | None = None, *, price_note: str = "", @@ -527,9 +522,9 @@ def _capacity_row( getting a row of its own because it is a PRICE, not a capability — there is no separate low-depth allowance, and printing one would imply a second balance.""" - detail = f"{cap.quota_used} / {cap.quota_total} quota" - if cap.bonus: - detail += f" + {cap.bonus} extra" + detail = f"{cap.used} / {cap.total} quota" + if cap.extra: + detail += f" + {cap.extra} extra" if cost: detail += f" · {_count(cost, 'credit')} each" if price_note: @@ -594,16 +589,16 @@ def render_usage(out: Output, u: Usage) -> None: assessments = _count(_equivalent(u, "assess"), "assessment") equivalents = f"≈ {verifications} · {assessments}" out.console.print(f" [bold]{u.credits.remaining} credits left[/bold] [dim]({equivalents})[/dim]") - _capacity_row(out, "Verify", u.verify, u.costs.get("verify"), price_note=_low_depth_note(u)) - _capacity_row(out, "Ask", u.ask, u.costs.get("ask")) - _capacity_row(out, "Assess", u.assess, u.costs.get("assess")) + _capacity_row(out, "Verify", _projection(u, "verify"), u.costs.get("verify"), price_note=_low_depth_note(u)) + _capacity_row(out, "Ask", _projection(u, "ask"), u.costs.get("ask")) + _capacity_row(out, "Assess", _projection(u, "assess"), u.costs.get("assess")) ex = u.extract label = f"{'Extract:':<9}" if ex.unlimited: out.console.print(f" {label} [dim]unlimited[/dim]") else: out.console.print(f" {label} {ex.calls_today} / {ex.daily_limit} today [dim](free — no credit charge)[/dim]") - resets_at = u.credits.resets_at or u.quota_resets_at + resets_at = u.credits.resets_at if resets_at: out.console.print(f" [dim]Credits reset {_humanize_reset(resets_at)}[/dim]") @@ -631,17 +626,29 @@ def _has_credit_pool(u: Usage) -> bool: return bool(c.total or c.remaining or c.used or c.extra or u.costs) +class _Projection(NamedTuple): + """The credit pool in one capability's unit (calls it would buy).""" + + used: int + total: int + remaining: int + extra: int + + +#: The price each capability's row is projected at when the price list leaves it out. +_DEFAULT_COSTS = {"verify": 10, "ask": 1, "assess": 1} + + +def _projection(u: Usage, capability: str) -> _Projection: + """``u.credits`` divided by the capability's price (``u.costs``).""" + cost = u.costs.get(capability) or _DEFAULT_COSTS.get(capability, 1) + total, left = u.credits.total // cost, u.credits.remaining // cost + return _Projection(max(0, total - left), total, left, u.credits.extra // cost) + + def _equivalent(u: Usage, capability: str) -> int: - """How many ``capability`` calls the remaining balance buys. - - The server already projects this per capability, so use its number; derive - from the price list only when the block is empty (a capability the server - reports a cost for but no projection — a new endpoint, say).""" - cap: UsageCapacity = getattr(u, capability, None) or UsageCapacity() - if cap.remaining or cap.quota_total: - return cap.remaining - cost = u.costs.get(capability) or 0 - return u.credits.remaining // cost if cost > 0 else 0 + """How many ``capability`` calls the remaining balance buys.""" + return _projection(u, capability).remaining def render_config(out: Output, payload: dict[str, Any]) -> None: diff --git a/src/lenz_io/cli/review.py b/src/lenz_io/cli/review.py index b605568..ec96d41 100644 --- a/src/lenz_io/cli/review.py +++ b/src/lenz_io/cli/review.py @@ -274,7 +274,7 @@ def _progress_cell(c: ReviewClaim) -> Any: if a.status in ("pending", "running", ""): return Spinner("dots", text=Text("quick check…", style="dim")) if a.status == "failed": - return Text(f"failed ({a.error_code or 'error'})", style="red") + return Text(f"failed ({(a.failure.code if a.failure else None) or 'error'})", style="red") quick = Text.from_markup(_verdict_text(a.verdict, a.confidence)) if v is None: return quick @@ -385,7 +385,7 @@ def _render_claim(out: Output, c: ReviewClaim, n: int) -> None: out.console.print(f"[bold]\\[{c.index + 1}/{n}][/bold] {escape(c.claim or '')}{_position_suffix(c)}") a, v = c.assessment, c.verification if a.status == "failed": - hint = (a.failure.hint if a.failure else None) or a.hint or a.error_code or "failed" + hint = (a.failure.hint if a.failure else None) or (a.failure.code if a.failure else None) or "failed" out.console.print(f" [red]Quick check failed:[/red] {escape(hint)}") return if v is not None and v.status == "completed": @@ -418,9 +418,7 @@ def _render_issue(out: Output, i: Any, n: int) -> None: if i.suggested_rewrite: out.console.print(f" [dim]Suggested rewrite:[/dim] {escape(i.suggested_rewrite)}") if i.failure is not None: - out.console.print( - f" [red]Deep check failed:[/red] {escape(i.failure.hint or i.failure.failure_reason or 'failed')}" - ) + out.console.print(f" [red]Deep check failed:[/red] {escape(i.failure.hint or i.failure.code or 'failed')}") elif i.source != "verification" and i.escalation is not None and i.escalation.disposition != "planned": out.console.print(f" [dim]Not deep-checked ({escape(i.escalation.disposition)}).[/dim]") if i.url: @@ -433,7 +431,7 @@ def render_review(out: Output, review: ReviewFull, *, issues_only: bool = False) or the reviewers' note (quick), and the suggested rewrite.""" out.console.print(_summary_line(review)) if review.failure is not None: - hint = review.failure.hint or review.failure.failure_reason or "no reason given" + hint = review.failure.hint or review.failure.code or "no reason given" out.console.print(f"[red]Failed:[/red] {escape(hint)}") n = review.summary.claims_selected or len(review.claims) if issues_only: @@ -621,7 +619,7 @@ def _render_citation_issue(out: Output, i: ReviewCitationIssue, total: int) -> N for d in i.metadata_differences: _print_text(out, f"Your reference says {d.cited or '(none)'}; the record says {d.registered or '(none)'}.") if i.failure is not None: - hint = i.failure.hint or i.failure.failure_reason or "failed" + hint = i.failure.hint or i.failure.code or "failed" out.console.print(f" [yellow]Part of the check failed:[/yellow] {escape(hint)}") diff --git a/src/lenz_io/cli/verify.py b/src/lenz_io/cli/verify.py index 8373375..465e44b 100644 --- a/src/lenz_io/cli/verify.py +++ b/src/lenz_io/cli/verify.py @@ -198,7 +198,7 @@ def _poll( # Sub-claims inherit the parent submission's depth server-side # (/select reads it off the task meta), so nothing to send here. items = client.select(task_id, claims=texts).items - picks = [(it.task_id, it.claim_text or txt) for it, txt in zip(items, texts, strict=True)] + picks = [(it.task_id, it.claim or txt) for it, txt in zip(items, texts, strict=True)] # detach, or >1 claim → batch path; exactly one → keep the # single-verdict flow (nicer than a 1-row table). if detach or len(picks) > 1: @@ -256,7 +256,7 @@ def _resolve_multi_claim(out: Output, task_id: str, st: TaskStatus, selection: l Otherwise json mode emits the needs_input object and exits 3 (never hangs on a prompt that can't happen); a TTY shows the checkbox picker. """ - options = [c.text for c in st.claims] + options = [c.claim for c in st.claims] if selection is not None: return [options[i] for i in _selection_to_indices(selection, len(options))] if out.json_mode: @@ -420,7 +420,7 @@ def _resume( if st.status == "completed": render_verification(out, st.result) elif st.status == "failed": - raise CLIError(st.error or "Verification failed.", code="pipeline_failed") + raise CLIError(st.error or st.failure_detail or "Verification failed.", code="pipeline_failed") else: # processing / needs_input → keep polling from here, honoring --claim/--detach _poll(client, out, ident, timeout, selection=selection, detach=detach) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 6122e22..a5fb01f 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -131,8 +131,10 @@ # The API version this SDK asks for, sent as ``X-Lenz-API-Version`` on every # request: the server answers in that version's response shape, whatever the # account. Since 3.0.0 this is the current shape (one name for each field, -# status and error code). The models still read the original ``2026-05-13`` -# shape as well, and every attribute keeps the meaning it had in 2.x. +# status and error code), and the SDK reads only that shape from its own +# calls (2.x asked for ``2026-05-13``). Every attribute keeps the meaning it +# had in 2.x; the old names are deprecated aliases. Webhooks are the one +# place both shapes are still parsed. API_VERSION = "2026-10-11" DEFAULT_BASE_URL = "https://lenz.io/api/v1" @@ -150,7 +152,7 @@ # charged it — and a retry with no idempotency key charged again. ASSESS_TIMEOUT = 100.0 #: Deprecated alias for :data:`ASSESS_TIMEOUT`, kept for callers that imported -#: it. Same value; removed no earlier than 3.0. +#: it. Same value; to be removed in a future major release. ASSESS_LIST_TIMEOUT = ASSESS_TIMEOUT # ``extract`` reads the whole input and enumerates its claims inside one # synchronous request. Most calls answer in seconds, but a long input can take @@ -231,13 +233,12 @@ def _batch_item_body(item: Any) -> Any: def _extracted(body: Any, *, locate: bool | None) -> ExtractedClaims: - """``/extract``'s answer. A newer-shape body (``claims``, no - ``identified_claims``) that located every claim away answers ``claims: - []``; the original field for that was ``locations=[]``, which only the + """``/extract``'s answer. A body that located every claim away answers + ``claims: []``; the 2.x field for that was ``locations=[]``, which only the request (``locate=True``) can tell apart from "nothing found".""" out = ExtractedClaims.model_validate(body) - newer = isinstance(body, dict) and "claims" in body and "identified_claims" not in body - if newer and locate and not body["claims"] and out.status == "not_a_claim" and out.locations is None: + answered = isinstance(body, dict) and body.get("claims") == [] + if answered and locate and out.status == "not_a_claim" and out.locations is None: out.locations = [] return out @@ -1215,7 +1216,7 @@ def review_and_wait( silent. Raises :class:`ReviewFailed` when the review ends ``failed`` (its - ``error_code`` and ``hint`` say why), and :class:`ReviewTimeout` when + ``failure`` says why), and :class:`ReviewTimeout` when ``timeout`` seconds pass first: the review keeps running, and the error carries its ``review_id`` and the last body read. """ @@ -1560,8 +1561,7 @@ def _verification_from_terminal(self, status: TaskStatus, task_id: str) -> Verif hint=status.hint, payload=status.model_dump(), ) - # failed. Server sends the diagnostic under ``error``; fall back to the - # legacy fields for resilience. + # failed. ``error`` is the 2.x sentence, rebuilt from ``failure``. detail = status.error or status.failure_detail or status.failure_reason or "unknown" if status.retryable: fix = "Transient provider outage — retry the same request after a short wait." diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 5016f84..fe8b483 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -24,6 +24,8 @@ import warnings from typing import TYPE_CHECKING, Any +from typing_extensions import deprecated + if TYPE_CHECKING: from .models import Citecheck, ReviewFull @@ -146,8 +148,9 @@ class LenzQuotaExceededError(LenzError): cost: int | None = None @property + @deprecated("Use `remaining`.", category=None) def credits_remaining(self) -> int: - """Deprecated alias for ``remaining``. Removed in 3.0. + """Deprecated: use `remaining`. To be removed in a future major release. Returns 0 when ``remaining`` is unknown, which is exactly the ambiguity ``remaining`` exists to fix — migrate to ``remaining``. @@ -179,7 +182,7 @@ def credits_remaining(self, value: int | None) -> None: @staticmethod def _warn_credits_remaining() -> None: warnings.warn( - "credits_remaining is deprecated and will be removed in 3.0; " + "credits_remaining is deprecated and will be removed in a future major release; " "use `remaining`, which is None when the server didn't report a " "balance (credits_remaining reports that as 0). It is NOT the " "API's `credits_remaining` body field — that is the credit pool, " @@ -517,13 +520,12 @@ def _parse_body(raw: bytes | str | None) -> dict[str, Any]: # ── The original error bodies, read from a newer-shape body ───────────── # -# The API's newer response shape gives every error one envelope: ``detail`` is -# a sentence, ``code`` is always set, a 422 lists its fields in ``errors``, and -# waits and links have one name each. The original shape differed by -# endpoint. ``_original_error`` rebuilds the original body by endpoint (the -# same rules as the Node SDK), so every attribute of the exception keeps its -# 2.x value; ``exc.body`` is the body as sent. Each rule matches only what the -# newer shape alone sends, so an original-shape body comes back unchanged. +# The API's current response shape (what 3.0 asks for) gives every error one +# envelope: ``detail`` is a sentence, ``code`` is always set, a 422 lists its +# fields in ``errors``, and waits and links have one name each. The 2.x shape +# differed by endpoint. ``_original_error`` rebuilds the 2.x body by endpoint +# (the same rules as the Node SDK), so every attribute of the exception keeps +# its 2.x value; ``exc.body`` is the body as sent. #: /review and /citecheck (submit and read) kept their own error envelope. _REVIEW_FAMILY = re.compile(r"^/(?:review|reviews/[^/]+|citecheck|citechecks/[^/]+)$") @@ -672,10 +674,10 @@ def map_response_to_error( Returns an *instance* (not raised) so callers can decide whether to raise, log, or surface. Keep this pure — no I/O. - ``endpoint`` is the request's ``(method, path)``: with it, an error in - the API's newer response shape gives the exception the same attributes - the original shape gave (``code``, ``message``, ``errors``, waits); - ``exc.body`` is always the body as sent. + ``endpoint`` is the request's ``(method, path)``: the client passes it, and + the exception gets the attributes 2.x gave (``code``, ``message``, + ``errors``, waits), which differed by endpoint; ``exc.body`` is the body + as sent. Without it the body is read as it stands. """ raw = _parse_body(body) parsed = raw @@ -724,12 +726,9 @@ def map_response_to_error( if status_code == 409 and isinstance(err, (LenzVerificationNotReadyError, LenzPipelineError)): # The generic 4xx advice ("retry; file an issue") is wrong for both: # the server's own hint says what to do, with a class default behind it. - # A failed run states its cause flat in the original response shape - # and in one ``failure`` block in the newer one. The block is read only - # for a newer-shape body, and only where the flat key is absent. + # A failed run states its cause in one ``failure`` block. raw_failure = parsed.get("failure") - newer = isinstance(raw_failure, dict) and "failure_reason" not in parsed - failure: dict[str, Any] = raw_failure if newer and isinstance(raw_failure, dict) else {} + failure: dict[str, Any] = raw_failure if isinstance(raw_failure, dict) else {} def _read(key: str, block_key: str) -> Any: return parsed[key] if key in parsed else failure.get(block_key) @@ -742,7 +741,7 @@ def _read(key: str, block_key: str) -> Any: else: reason = _opt_str(_read("failure_reason", "code")) # The original spelling of "nothing checkable" on a verification. - err.failure_reason = "not_a_claim" if newer and reason == "no_checkable_claim" else reason + err.failure_reason = "not_a_claim" if reason == "no_checkable_claim" else reason err.failure_class = _opt_str(_read("failure_class", "failure_class")) # Only a real boolean is a retry signal, as in the wait path. retryable = _read("retryable", "retryable") @@ -784,9 +783,9 @@ def _read(key: str, block_key: str) -> Any: # sending null precisely so "unknown" stays distinguishable from # "zero". Collapsing that here would throw the distinction away. err.remaining = _opt_int(parsed.get("remaining")) - if "remaining" not in parsed and "requested" not in parsed and "doc_url" not in parsed and "docs_url" in parsed: - # A newer-shape body (``docs_url``) without the capability figure: - # the pool divided by this one call's price. + if "remaining" not in parsed and "requested" not in parsed: + # A body without the capability figure: the pool divided by this + # one call's price. pool, price = _opt_int(parsed.get("credits_remaining")), _opt_int(parsed.get("cost")) if pool is not None and price: err.remaining = pool // price @@ -796,14 +795,8 @@ def _read(key: str, block_key: str) -> Any: # NOT on the same-named deprecated property — that one aliases # ``remaining`` and means a different quantity (see the class docstring). err.credit_balance = _opt_int(parsed.get("credits_remaining")) - if ( - err.credit_balance is None - and err.remaining is not None - and "remaining" in parsed - and "docs_url" in parsed - and "doc_url" not in parsed - ): - # A newer-shape body leaves the pool out where it equals + if err.credit_balance is None and err.remaining is not None and "remaining" in parsed: + # The current shape leaves the pool out where it equals # ``remaining`` (a citation check costs one credit). err.credit_balance = err.remaining err.cost = _opt_int(parsed.get("cost")) @@ -825,13 +818,10 @@ def _read(key: str, block_key: str) -> Any: "reset_in_seconds" not in parsed and "retry_after_seconds" not in parsed and "retry_after" in parsed - and "docs_url" in parsed - and "doc_url" not in parsed and code not in _IN_FLIGHT_429_CODES ): - # A newer-shape body (``docs_url``) names every wait - # ``retry_after``. The in-flight refusals never carried - # ``reset_in_seconds``. + # The current shape names every wait ``retry_after``. The + # in-flight refusals never carried ``reset_in_seconds``. err.reset_in_seconds = _opt_int(parsed.get("retry_after")) rl_upgrade_url = parsed.get("upgrade_url") err.upgrade_url = rl_upgrade_url if isinstance(rl_upgrade_url, str) else "" diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index f8194ef..98284ef 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -23,6 +23,7 @@ from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, model_validator +from typing_extensions import deprecated class _Lax(BaseModel): @@ -37,24 +38,27 @@ class _Lax(BaseModel): model_config = ConfigDict(extra="allow") -# ── Reading both response shapes ───────────────────────────────────────── +# ── Reading the current response shape ─────────────────────────────────── # -# The API is gaining a second, dated response shape that renames a handful of +# Since 3.0 the SDK asks for the API's 2026-10-11 response shape and reads +# only that shape from its own calls (webhooks of both shapes are still +# parsed; see ``webhooks.py``). The current shape renames a handful of 2.x # fields (``claim_text`` -> ``claim``, ``modified_at`` -> ``completed_at``, a # failed item's ``error`` / ``error_code`` / ``failure_reason`` -> one -# ``failure`` block, ...). This release still asks for the original shape. +# ``failure`` block, ...). # -# Two rules keep existing code exactly as it was: +# Every 2.x name stays as a DEPRECATED alias with its 2.x value, so 2.x code +# runs unchanged: # -# * A body is treated as the newer shape only when it carries something only -# that shape has. Every other body is parsed exactly as before: same fields, -# same values, same dump, same ``exclude_unset``. -# * A newer-shape body gets the original fields filled in, with their original -# meaning, from the newer ones (only where the body does not carry them). +# * The 2.x fields are model fields, filled from the current ones in a +# ``before`` validator (only where the body does not carry them). +# * The current names are read-only properties over what the server sent. +# They are not model fields, so ``model_dump()`` shows the body as sent plus +# the 2.x fields. # -# The newer names are read-only properties, computed from either shape: they -# are not model fields, so ``repr``, ``model_dump()``, equality and pickling of -# an original-shape object are unchanged. +# A few models are also built from webhook payloads of the original shape +# (a review, a citation check, a failure block, a needs-input option); those +# keep reading it. #: The one code for "the input holds nothing that can be checked", in the #: newer response shape. The original shape spells it per endpoint: @@ -73,10 +77,9 @@ def _old_code(code: Any, old: str) -> Any: return old if code in (NO_CHECKABLE_CLAIM, *_OLD_NO_CLAIM_CODES) else code -def _is_newer(data: Any, has: str, lacks: str) -> bool: - """A dict carrying ``has`` (a key only the newer shape sends) and not - ``lacks`` (its original-shape counterpart).""" - return isinstance(data, dict) and has in data and lacks not in data +def _is_newer(data: Any, key: str) -> bool: + """A dict carrying ``key`` (a key only the current response shape sends).""" + return isinstance(data, dict) and key in data def _utc_day(iso: Any) -> Any: @@ -104,8 +107,8 @@ def _modified_at_from(created_at: Any, completed_at: Any) -> str | None: def _fill_modified_at(data: Any) -> Any: """A newer-shape verification (``completed_at``, no ``modified_at``): add the original ``modified_at``.""" - if _is_newer(data, "completed_at", "modified_at"): - return {**data, "modified_at": _modified_at_from(data.get("created_at"), data.get("completed_at"))} + if _is_newer(data, "completed_at"): + return _fill(data, modified_at=_modified_at_from(data.get("created_at"), data.get("completed_at"))) return data @@ -199,19 +202,20 @@ class Audit(_Lax): class CandidateClaim(_Lax): """One of multiple distinct claims framing found in the submitted text.""" + #: **Deprecated**, use :attr:`claim` (the same string). text: str = "" domain: str = "" @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if _is_newer(data, "claim", "text"): + if _is_newer(data, "claim"): return _fill(data, text=data["claim"]) return data @property def claim(self) -> str: - """The option's claim (``text`` is its older name, the same string).""" + """The option's claim. Replaces ``text``, which is deprecated and kept.""" sent = _sent(self, "claim") return sent if isinstance(sent, str) else self.text @@ -334,8 +338,9 @@ class Verification(_Lax): endpoint, and the webhook payload. The verdict block is FLAT at top level (was nested ``Verdict`` object - pre-unify). ``created_at`` + ``modified_at`` are the only timestamp - fields on the API surface — editorial ``published_at`` is internal-only. + pre-unify). ``created_at`` + ``completed_at`` are the only timestamp + fields on the API surface (``modified_at`` is the deprecated 2.x name of + the second) — editorial ``published_at`` is internal-only. 1.1.0: dropped ``url`` and ``visibility``. API claims are private by default and referenced by ``verification_id`` only. Cache-hit on @@ -384,6 +389,8 @@ class Verification(_Lax): sources: list[Source] = Field(default_factory=list) audit: Audit = Field(default_factory=Audit) created_at: str | None = None + # Deprecated: use ``completed_at``. Set only when the verification + # completed on a later UTC day than ``created_at``. modified_at: str | None = None # Output language (ISO 639-1). Always populated by the server when # the SDK is fresh; defaulted to ``'en'`` for resilience against @@ -406,11 +413,11 @@ def _read_newer_shape(cls, data: Any) -> Any: @property def completed_at(self) -> str | None: - """When the verification completed. From the original response shape it - is known only through ``modified_at``: set when that is, ``None`` on a - same-day completion.""" + """When the verification completed. Replaces ``modified_at``, which is + deprecated and kept (set only when the completion fell on a later UTC + day than ``created_at``).""" sent = _sent(self, "completed_at") - return sent if isinstance(sent, str) else self.modified_at + return sent if isinstance(sent, str) else None class VerificationListItem(_Lax): @@ -432,6 +439,7 @@ class VerificationListItem(_Lax): # A suggested rewrite of ``claim``. See ``Verification.suggested_rewrite``. suggested_rewrite: str | None = None created_at: str | None = None + # Deprecated: use ``completed_at``. See ``Verification.modified_at``. modified_at: str | None = None # Output language (ISO 639-1). See ``Verification.language``. language: str = "en" @@ -443,11 +451,11 @@ def _read_newer_shape(cls, data: Any) -> Any: @property def completed_at(self) -> str | None: - """When the verification completed. From the original response shape it - is known only through ``modified_at``: set when that is, ``None`` on a - same-day completion.""" + """When the verification completed. Replaces ``modified_at``, which is + deprecated and kept (set only when the completion fell on a later UTC + day than ``created_at``).""" sent = _sent(self, "completed_at") - return sent if isinstance(sent, str) else self.modified_at + return sent if isinstance(sent, str) else None class VerificationList(_Lax): @@ -505,8 +513,7 @@ class ClaimLocation(_Lax): """Where the submitted text makes one claim. ``claim`` is exactly as in the claim list it belongs to - (``ExtractedClaims.claim`` / ``identified_claims``, or a review's - ``more_claims``). ``positions`` lists every place the text makes it, in + (``ExtractedClaims.claims``, or a review's ``more_claims``). ``positions`` lists every place the text makes it, in text order, at most 10. It is ``None`` only when that claim could not be placed; on ``/extract`` every returned claim is placed. """ @@ -550,21 +557,25 @@ class ExtractedClaims(_Lax): ``status`` is one of ``ExtractStatus``: ``ready``, ``not_a_claim``, or — when a ``focus`` was given and no claim fell within it — ``no_match``. - ``no_match`` is a successful answer, not an error: ``identified_claims`` - is empty and the unfocused list is never substituted for it. - - ``locations`` is set only on a call made with ``locate=True``: one - ``ClaimLocation`` per returned claim, in the order of - ``identified_claims`` (one entry for a single ``claim``). It is ``[]`` - when every claim was left out (``status`` is then ``"not_a_claim"``), - and ``None`` when ``locate`` was not set, when the extraction found no - claims, or when the - claims could not be located (the list is then returned unfiltered). - Older servers omit the key; it parses as ``None``. + ``no_match`` is a successful answer, not an error: ``claims`` is empty + and the unfocused list is never substituted for it. + + ``claims`` lists every claim found, most check-worthy first (one entry + for one claim). On a call made with ``locate=True`` each carries its + ``positions`` (the claims that could not be placed are left out, and + ``status`` is ``"not_a_claim"`` when none is left); they are ``None`` + when ``locate`` was not set or the claims could not be located (the list + is then returned unfiltered). + + ``claim``, ``identified_claims`` and ``locations`` are the deprecated 2.x + fields, kept with their 2.x values: ``claims`` is their replacement. """ + # ``"not_a_claim"`` is the 2.x spelling of the API's ``no_checkable_claim``. status: str = "" + # Deprecated: use ``claims[0].claim``. claim: str = "" + # Deprecated: use ``claims`` (this is its names, and ``[]`` for one claim). identified_claims: list[str] = Field(default_factory=list) # Deprecated: always empty since 2026-09-12. Kept because the server # still sends the key. @@ -573,12 +584,13 @@ class ExtractedClaims(_Lax): key_entities: list[ExtractedEntity] = Field(default_factory=list) presumed_intent: str = "" original_input: str = "" + # Deprecated: use ``claims`` (each with its ``positions``). locations: list[ClaimLocation] | None = None @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if not (_is_newer(data, "claims", "identified_claims") and isinstance(data["claims"], list)): + if not (_is_newer(data, "claims") and isinstance(data["claims"], list)): return data items = [c for c in data["claims"] if isinstance(c, dict)] names = [c.get("claim") if isinstance(c.get("claim"), str) else "" for c in items] @@ -599,17 +611,10 @@ def _read_newer_shape(cls, data: Any) -> Any: def claims(self) -> list[ExtractedClaim]: """Every claim found, most check-worthy first: always a list (one entry for one claim, ``[]`` for none), each with its ``positions`` when the - call located them. Read from either response shape.""" + call located them. Replaces ``claim``, ``identified_claims`` and + ``locations``, which are deprecated and kept.""" sent = _sent(self, "claims") - if isinstance(sent, list): - return [ExtractedClaim.model_validate(c) for c in sent if isinstance(c, dict)] - names = list(self.identified_claims) or ([self.claim] if self.claim else []) - if self.claim and self.claim not in names: - names.insert(0, self.claim) - positions: dict[str, Any] = {} - for loc in self.locations or []: - positions.setdefault(loc.claim, loc.positions) - return [ExtractedClaim(claim=n, positions=positions.get(n)) for n in names] + return [ExtractedClaim.model_validate(c) for c in sent if isinstance(c, dict)] if isinstance(sent, list) else [] #: The original shape's ``hint`` on an /assess verdict row that found other @@ -626,16 +631,19 @@ class AssessClaim(_Lax): ``ClaimDetailOut`` payload at ``GET /api/v1/verifications/{id}`` for callers that want citations and the full audit trail. - A row with ``verdict == "Error"`` could not be given a verdict. On a + A row with ``status == "failed"`` could not be given a verdict. On a list call it stays in position (one row per item sent), is not charged, - and says why: ``error_code`` names the cause and ``hint`` is one - sentence on what to send next. ``hint`` is the field to surface to a - human — it is written per cause and stays correct as causes are added. + and says why: ``failure.code`` names the cause and ``failure.hint`` is + one sentence on what to send next (``error_code`` and ``hint`` are their + 2.x names, deprecated and kept; so is reading ``verdict == "Error"`` + instead of ``status == "failed"``). ``failure.hint`` is the field to + surface to a human — it is written per cause and stays correct as causes + are added. + A vague input is assessed on its most likely reading, which ``claim`` carries. A compound input is assessed on its main claim; the other - claims found in it are listed in ``identified_claims`` (also with a - ``hint``). These fields default empty so older servers that don't send - them still parse. + claims found in it are listed in ``more_claims`` (``identified_claims``, + deprecated and kept). """ claim: str = "" @@ -661,8 +669,9 @@ class AssessClaim(_Lax): # check's reasoning and not itself verified: review it, or run it through # ``verify``, before using it. suggested_rewrite: str | None = None - # Only on ``verdict == "Error"`` rows: 'no_claim' | 'framing_failed' | - # 'upstream_unavailable' | 'timeout'. + # Deprecated: use ``failure.code`` (``"no_checkable_claim"`` where this + # reads ``"no_claim"``). Only on ``verdict == "Error"`` rows: + # 'no_claim' | 'framing_failed' | 'upstream_unavailable' | 'timeout'. # # An OPEN set, deliberately typed ``str`` rather than a Literal: the API # may add a cause in a minor version, so branch on the ones you know and @@ -678,16 +687,19 @@ class AssessClaim(_Lax): # Deprecated: always empty since 2026-09-12, when the ``ambiguous`` cause # that filled it was retired. Kept because the server still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) - # Other claims found in the input that were NOT assessed; else empty. + # Deprecated: use ``more_claims``. Other claims found in the input that + # were NOT assessed; else empty. identified_claims: list[str] = Field(default_factory=list) - # One sentence on what to send next. Set on every Error row and on a row - # with a non-empty ``identified_claims``; ``None`` on a plain verdict row. + # Deprecated: use ``failure.hint`` (on a failed row; the sentence on a + # verdict row that found other claims has no replacement). One sentence on + # what to send next. Set on every Error row and on a row with a non-empty + # ``identified_claims``; ``None`` on a plain verdict row. hint: str | None = None @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if not _is_newer(data, "failure", "error_code"): + if not _is_newer(data, "failure"): return data out = dict(data) failed = out.get("status") == "failed" @@ -711,32 +723,27 @@ def _read_newer_shape(cls, data: Any) -> Any: @property def status(self) -> str: - """``"completed"`` (a verdict) or ``"failed"`` (none; see ``failure``).""" + """``"completed"`` (a verdict) or ``"failed"`` (none; see ``failure``). + Replaces ``verdict == "Error"``, which is deprecated and kept.""" sent = _sent(self, "status") - if isinstance(sent, str): - return sent - if self.verdict == "Error": - return "failed" - return "completed" if self.verdict else "" + return sent if isinstance(sent, str) else "" @property def failure(self) -> FailureBlock | None: """Why a failed row has no verdict (``None`` on a completed row): ``code`` (``no_checkable_claim`` | ``framing_failed`` | ``upstream_unavailable`` | ``timeout`` today, an open set) and ``hint``, - one sentence on what to send next.""" + one sentence on what to send next. Replaces ``error_code`` and + ``hint``, which are deprecated and kept.""" sent = _sent(self, "failure") - if isinstance(sent, dict): - return FailureBlock.model_validate(sent) - if self.status != "failed": - return None - return FailureBlock(failure_reason=self.error_code or "", hint=self.hint) + return FailureBlock.model_validate(sent) if isinstance(sent, dict) else None @property def more_claims(self) -> list[str]: - """Other claims found in the input that were not assessed.""" + """Other claims found in the input that were not assessed. Replaces + ``identified_claims``, which is deprecated and kept.""" sent = _sent(self, "more_claims") - return list(sent) if isinstance(sent, list) else list(self.identified_claims) + return list(sent) if isinstance(sent, list) else [] #: The ``AssessResponse.status`` values this release knows about. A @@ -764,14 +771,18 @@ class AssessResponse(_Lax): ``"Error"`` row in position (see ``AssessClaim``), never a missing one; the top-level ``error`` stays ``None``. - When ``claims`` is empty (single form), ``error_code`` is ``'no_claim'``: - the input holds no checkable claim (a vague input is assessed on its - most likely reading instead). It defaults empty, so older servers that - don't send it degrade to the plain ``error`` message. + When ``claims`` is empty (single form), ``status`` is + ``"no_checkable_claim"`` and ``failure`` says why: the input holds no + checkable claim (a vague input is assessed on its most likely reading + instead). The deprecated ``error`` and ``error_code`` (``'no_claim'``) + keep their 2.x values. """ claims: list[AssessClaim] = Field(default_factory=list) + # Deprecated: use ``failure`` / ``status``. The 2.x sentence for an input + # with no claim. error: str | None = None + # Deprecated: use ``failure.code`` (``"no_checkable_claim"``). error_code: str = "" # '' | 'no_claim' # Deprecated: always empty since 2026-09-12. Kept because the server # still sends the key. @@ -783,7 +794,7 @@ class AssessResponse(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if not _is_newer(data, "failure", "error"): + if not _is_newer(data, "failure"): return data failure = data.get("failure") if isinstance(data.get("failure"), dict) else None if failure is None: @@ -799,54 +810,47 @@ def _read_newer_shape(cls, data: Any) -> Any: def status(self) -> str: """``ok`` (some row has a verdict), ``no_checkable_claim`` (the input, or every item, holds nothing checkable) or ``error``. See - ``AssessStatus``; computed from the rows when a server does not send it.""" + ``AssessStatus``. Replaces ``error`` and ``error_code``, which are + deprecated and kept.""" sent = _sent(self, "status") - if isinstance(sent, str): - return sent - rows = self.claims - if any(r.status == "completed" for r in rows): - return "ok" - if not rows: - return NO_CHECKABLE_CLAIM if _new_code(self.error_code) == NO_CHECKABLE_CLAIM else "error" - codes = {r.failure.code if r.failure else None for r in rows} - return NO_CHECKABLE_CLAIM if codes == {NO_CHECKABLE_CLAIM} else "error" + return sent if isinstance(sent, str) else "" @property def failure(self) -> FailureBlock | None: - """Why the single form has no rows; ``None`` otherwise.""" + """Why the single form has no rows; ``None`` otherwise. Replaces + ``error`` and ``error_code``, which are deprecated and kept.""" sent = _sent(self, "failure") - if isinstance(sent, dict): - return FailureBlock.model_validate(sent) - if not (self.error or self.error_code): - return None - return FailureBlock.model_validate({"failure_reason": self.error_code, "detail": self.error}) + return FailureBlock.model_validate(sent) if isinstance(sent, dict) else None class TaskAccepted(_Lax): """Returned by ``POST /verify`` and per item of ``POST /verify/batch``.""" task_id: str = "" + #: **Deprecated**, use :attr:`claim`. claim_text: str = "" @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if _is_newer(data, "claim", "claim_text"): + if _is_newer(data, "claim"): return _fill(data, claim_text=data["claim"]) return data @property def claim(self) -> str: """The item's claim on a batch or select receipt (``""`` on a single - ``verify`` receipt); ``claim_text`` is its older name.""" + ``verify`` receipt). Replaces ``claim_text``, which is deprecated and + kept.""" sent = _sent(self, "claim") - return sent if isinstance(sent, str) else self.claim_text + return sent if isinstance(sent, str) else "" @property + @deprecated("It is no longer sent; use `task_id` to poll.", category=None) def chain_id(self) -> str: - """An internal correlation id the original response shape sent on a - ``verify`` receipt; ``""`` when the response carries none (the current - shape never does). It cannot be polled: use ``task_id``.""" + """Deprecated: no longer sent. An internal correlation id the 2.x + response shape sent on a ``verify`` receipt; ``""`` now. It cannot be + polled: use ``task_id``.""" sent = _sent(self, "chain_id") return sent if isinstance(sent, str) else "" @@ -875,8 +879,9 @@ class Progress(_Lax): passes it through. This was a plain ``dict`` up to 2.10.0. The mapping methods below keep - ``p["step"]``, ``"step" in p``, ``p.keys()`` and ``dict(p)`` working on a - 2.x minor; they are deprecated and go in 3.0.0. Use attributes. + ``p["step"]``, ``"step" in p``, ``p.keys()`` and ``dict(p)`` working; they + are deprecated and will be removed in a future major release. Use + attributes. """ step: str = "" @@ -885,7 +890,7 @@ class Progress(_Lax): elapsed_seconds: int | None = None poll_after_seconds: int | None = None - # ── dict compatibility shim (deprecated, removed in 3.0.0) ── + # ── dict compatibility shim (deprecated, to be removed in a future major) ── # # `.get()` alone would have been the worst option available: the # changelog would say "minor", the shim would look like it handled @@ -962,11 +967,12 @@ class TaskStatus(_Lax): # the JSON schema only, so reading them does not warn. candidates: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) similar_claims: list[SimilarVerification] = Field(default_factory=list, json_schema_extra={"deprecated": True}) - # failure branches. The server's failed response is - # ``{"status": "failed", "error": "..."}`` — ``error`` is the live wire - # field. ``failure_reason`` / ``failure_detail`` are kept for forward/back - # compatibility and other channels; read precedence is - # ``error or failure_detail or failure_reason``. + # failure branches. Deprecated: ``error``, ``failure_reason``, + # ``failure_detail``, ``failure_class``, ``retryable`` and ``docs_url`` + # below are the 2.x flat fields of a failed run; read ``failure`` instead + # (``failure.detail``, ``.code``, ``.failure_class``, ...). ``error`` keeps + # its 2.x sentence; ``failure_detail`` is always empty, with no + # replacement. error: str = "" failure_reason: str = "" failure_detail: str = "" @@ -984,7 +990,7 @@ class TaskStatus(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if not _is_newer(data, "failure", "failure_reason") or not isinstance(data["failure"], dict): + if not _is_newer(data, "failure") or not isinstance(data["failure"], dict): return data failure = data["failure"] reason = _old_code(failure.get("code"), "not_a_claim") @@ -1004,22 +1010,12 @@ def failure(self) -> FailureBlock | None: """On ``failed``: why. ``code`` is the cause (an open set, e.g. ``research_empty``, ``no_checkable_claim``), ``detail`` one sentence on what happened, ``failure_class`` / ``retryable`` / ``hint`` / - ``docs_url`` as on the original fields. ``None`` on other statuses.""" + ``docs_url``. ``None`` on other statuses. Replaces ``error``, + ``failure_reason``, ``failure_detail``, ``failure_class``, + ``retryable``, ``docs_url`` and ``hint`` on a failure, which are + deprecated and kept.""" sent = _sent(self, "failure") - if isinstance(sent, dict): - return FailureBlock.model_validate(sent) - if self.status != "failed": - return None - return FailureBlock.model_validate( - { - "failure_reason": self.failure_reason, - "detail": self.error or self.failure_detail or None, - "failure_class": self.failure_class, - "retryable": self.retryable, - "hint": self.hint or None, - "docs_url": self.docs_url, - } - ) + return FailureBlock.model_validate(sent) if isinstance(sent, dict) else None class BatchItemResult(_Lax): @@ -1039,6 +1035,7 @@ class BatchItemResult(_Lax): """ task_id: str = "" + #: **Deprecated**, use :attr:`claim`. claim_text: str = "" status: Literal["completed", "needs_input", "failed", "timeout"] verification: Verification | None = None @@ -1046,7 +1043,8 @@ class BatchItemResult(_Lax): @property def claim(self) -> str: - """The item's claim (``claim_text`` is its older name).""" + """The item's claim. Replaces ``claim_text``, which is deprecated and + kept.""" return self.claim_text @@ -1087,18 +1085,9 @@ class UsageCredits(_Lax): @model_validator(mode="before") @classmethod def _mirror_extra_and_bonus(cls, data: Any) -> Any: - """Keep ``extra`` and its deprecated old name in step, in both directions. - - A server that has not started sending ``extra`` sends only ``bonus``; - the newer response shape sends only ``extra``. Mirroring - here means both attributes read correctly either way. - """ - if not isinstance(data, dict): - return data - has_extra, has_bonus = data.get("extra") is not None, data.get("bonus") is not None - if has_bonus and not has_extra: - return {**data, "extra": data["bonus"]} - if has_extra and not has_bonus: + """Fill the deprecated ``bonus`` from ``extra``, the one number the + current response shape sends.""" + if isinstance(data, dict) and data.get("extra") is not None and data.get("bonus") is None: return {**data, "bonus": data["extra"]} return data @@ -1160,20 +1149,10 @@ class UsageCapacity(_Lax): @model_validator(mode="before") @classmethod def _mirror_bonus_and_credits(cls, data: Any) -> Any: - """Keep ``bonus`` and its deprecated alias in step, in both directions. - - A server predating the credit pool sends only ``credits``; a block - computed from the pool has only ``bonus``. Mirroring here means - both attributes read correctly either way, so the SDK never depends on - which side of an API deploy it is talking to. - """ - if not isinstance(data, dict): - return data - has_bonus, has_credits = data.get("bonus") is not None, data.get("credits") is not None - if has_bonus and not has_credits: + """Fill the deprecated ``credits`` alias from ``bonus``: the blocks are + computed from the pool, so they carry only ``bonus``.""" + if isinstance(data, dict) and data.get("bonus") is not None and data.get("credits") is None: return {**data, "credits": data["bonus"]} - if has_credits and not has_bonus: - return {**data, "bonus": data["credits"]} return data @@ -1219,6 +1198,7 @@ class Usage(_Lax): #: comparing against it will break on a rename that ought to be free. #: Empty on servers predating this field — fall back to :attr:`plan`. plan_label: str = "" + #: **Deprecated**, use :attr:`UsageCredits.resets_at` (``credits.resets_at``). quota_resets_at: str | None = None #: The credit balance — the authoritative number. Empty on older servers. credits: UsageCredits = Field(default_factory=UsageCredits) @@ -1280,8 +1260,6 @@ def _read_newer_shape(cls, data: Any) -> Any: did, and ``quota_resets_at`` from ``credits.resets_at``.""" if not (isinstance(data, dict) and isinstance(data.get("credits"), dict)): return data - if any(k in data for k in ("verify", "ask", "assess", "quota_resets_at")): - return data credits, costs = data["credits"], data.get("costs") def _int(value: Any) -> int: @@ -1291,8 +1269,10 @@ def _int(value: Any) -> int: if not isinstance(costs, dict): costs = {} total, remaining = _int(credits.get("total")), _int(credits.get("remaining")) - extra = _int(credits.get("extra", credits.get("bonus"))) + extra = _int(credits.get("extra")) for capability, default_cost in _BLOCK_COSTS: + if capability in out: + continue # At the price the body publishes; a capability it leaves out (or # prices at 0) at the price the blocks always used. cost = _int(costs.get(capability)) or default_cost @@ -1376,14 +1356,17 @@ class ReviewStarted(_Lax): class FailureBlock(_Lax): """Why a review, or one claim's work inside it, failed. - The same fields a failed ``GET /verify/status`` carries: ``failure_reason`` - is the specific cause (an open set, e.g. ``no_claim``, - ``insufficient_credits``, ``timeout``), ``failure_class`` the closed + The same fields a failed ``GET /verify/status`` carries: ``code`` + is the specific cause (an open set, e.g. ``no_checkable_claim``, + ``insufficient_credits``, ``timeout``; ``failure_reason`` is its 2.x name, + deprecated and kept, spelling "nothing checkable" ``no_claim`` or + ``not_a_claim``), ``failure_class`` the closed :data:`FailureClass`, ``retryable`` whether resending the same request can help, ``hint`` one sentence on what to do next and ``docs_url`` where the class is explained. """ + #: **Deprecated**, use :attr:`code`. failure_reason: str | None = "" failure_class: str | None = "" retryable: bool | None = None @@ -1393,15 +1376,15 @@ class is explained. @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if _is_newer(data, "code", "failure_reason"): + if _is_newer(data, "code"): return _fill(data, failure_reason=_old_code(data["code"], "no_claim")) return data @property def code(self) -> str | None: """The cause, an open set (``no_checkable_claim``, ``timeout``, ...). - ``failure_reason`` is its older name, spelling "nothing checkable" - ``no_claim``.""" + Replaces ``failure_reason``, which is deprecated and kept (it spells + "nothing checkable" ``no_claim``).""" sent = _sent(self, "code") return sent if isinstance(sent, str) else _new_code(self.failure_reason) @@ -1417,7 +1400,7 @@ def _deep_check_failure(data: Any) -> Any: "nothing checkable" was ``not_a_claim`` (a quick check's and the review's own, ``no_claim``, which ``FailureBlock`` reads by default).""" failure = data.get("failure") if isinstance(data, dict) else None - if isinstance(failure, dict) and _is_newer(failure, "code", "failure_reason"): + if isinstance(failure, dict) and _is_newer(failure, "code"): return {**data, "failure": _fill(failure, failure_reason=_old_code(failure["code"], "not_a_claim"))} return data @@ -1492,7 +1475,8 @@ class ReviewSummary(_Lax): claims_selected: int | None = None #: The resolved ``max_assessments``. claim_limit: int = 20 - #: The draft held at least ``claim_limit`` claims: more MAY exist. + #: **Deprecated**, use :attr:`claim_limit_exceeded`. The draft held at + #: least ``claim_limit`` claims: more MAY exist. claim_limit_reached: bool | None = None #: The text was cut at 50,000 characters. input_truncated: bool = False @@ -1505,6 +1489,7 @@ class ReviewSummary(_Lax): citations_selected: int | None = None #: The resolved ``max_citations``. citation_limit: int | None = None + #: **Deprecated**, use :attr:`citation_limit_exceeded`. #: ``citations_found`` is over ``citation_limit``. citation_limit_reached: bool | None = None citation_checks: ReviewCitationCheckCounts | None = None @@ -1518,7 +1503,7 @@ class ReviewSummary(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if not _is_newer(data, "claim_limit_exceeded", "claim_limit_reached"): + if not _is_newer(data, "claim_limit_exceeded"): return data found, limit = data.get("claims_found"), data.get("claim_limit", 20) reached = found >= limit if isinstance(found, int) and isinstance(limit, int) else None @@ -1568,8 +1553,9 @@ class ReviewResult(_Lax): class ReviewAssessment(_Lax): """A claim's quick check. ``status``: ``pending`` | ``running`` | ``completed`` | ``failed``. On ``completed`` the fields mean what they - mean on an ``/assess`` row; on ``failed`` see ``error_code`` (an open - set, as on ``/assess``), ``hint`` and ``failure``.""" + mean on an ``/assess`` row; on ``failed`` see ``failure`` (``code`` is an + open set, as on ``/assess``; ``error_code`` and ``hint`` are its 2.x + names, deprecated and kept).""" status: str = "" verdict: str | None = None @@ -1577,8 +1563,12 @@ class ReviewAssessment(_Lax): rationale: str | None = None dissent: str | None = None verification_url: str | None = None + #: **Deprecated**, use ``failure.code`` (``no_checkable_claim`` where this + #: reads ``no_claim``). error_code: str | None = None + #: **Deprecated**, use :attr:`more_claims`. identified_claims: list[str] = Field(default_factory=list) + #: **Deprecated**, use ``failure.hint`` (on a failed quick check). hint: str | None = None #: With ``suggest_edits=True``: the claim with its wrong part corrected, #: from the quick check's reasoning, when it found the claim "False" or @@ -1591,7 +1581,7 @@ class ReviewAssessment(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if not _is_newer(data, "more_claims", "identified_claims"): + if not _is_newer(data, "more_claims"): return data failure = data.get("failure") if isinstance(data.get("failure"), dict) else {} return _fill( @@ -1602,7 +1592,8 @@ def _read_newer_shape(cls, data: Any) -> Any: @property def more_claims(self) -> list[str]: - """Other claims found in the passage that were not checked.""" + """Other claims found in the passage that were not checked. Replaces + ``identified_claims``, which is deprecated and kept.""" sent = _sent(self, "more_claims") return list(sent) if isinstance(sent, list) else list(self.identified_claims) @@ -1646,6 +1637,8 @@ class ReviewVerification(_Lax): suggested_rewrite: str | None = None warnings: list[str] = Field(default_factory=list) created_at: str | None = None + #: **Deprecated**, use :attr:`completed_at`. Set only when the check + #: completed on a later UTC day than ``created_at``. modified_at: str | None = None verification_url: str | None = None url: str | None = None @@ -1977,6 +1970,7 @@ class CitecheckSummary(_Lax): citations_found: int | None = None citations_selected: int | None = None citation_limit: int | None = None + #: **Deprecated**, use :attr:`citation_limit_exceeded`. citation_limit_reached: bool | None = None citation_checks: ReviewCitationCheckCounts | None = None citation_issues: int = 0 @@ -1984,7 +1978,7 @@ class CitecheckSummary(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if _is_newer(data, "citation_limit_exceeded", "citation_limit_reached"): + if _is_newer(data, "citation_limit_exceeded"): return _fill(data, citation_limit_reached=data["citation_limit_exceeded"]) return data diff --git a/tests/fixtures/parity/canonical/extract__replay_later_200.json b/tests/fixtures/parity/canonical/extract__replay_later_200.json index 8f23ae8..d1b6b07 100644 --- a/tests/fixtures/parity/canonical/extract__replay_later_200.json +++ b/tests/fixtures/parity/canonical/extract__replay_later_200.json @@ -2,12 +2,16 @@ "status": 200, "body": { "status": "ready", - "claim": "Alpha rose 5% in 2024.", - "identified_claims": [ - "Alpha rose 5% in 2024.", - "Beta fell 3% last year." + "claims": [ + { + "claim": "Alpha rose 5% in 2024.", + "positions": null + }, + { + "claim": "Beta fell 3% last year.", + "positions": null + } ], - "candidate_claims": [], "domain": "Economics", "key_entities": [ { @@ -20,8 +24,7 @@ } ], "presumed_intent": "Verify reported figures", - "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year.", - "locations": null + "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." }, "headers": { "Content-Type": "application/json", diff --git a/tests/test_cli.py b/tests/test_cli.py index 1051d39..490e746 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -482,9 +482,7 @@ def test_extract_pretty_renders_positions(): extracted = ExtractedClaims.model_validate( { "status": "ready", - "claim": "A rose 5%.", - "identified_claims": ["A rose 5%.", "B fell [bold]."], - "locations": [ + "claims": [ {"claim": "A rose 5%.", "positions": [{"start": 0, "end": 11, "text": "A grew 5 %."}]}, {"claim": "B fell [bold].", "positions": [{"start": 12, "end": 26, "text": "B fell [bold]."}]}, ], @@ -500,9 +498,7 @@ def test_extract_pretty_renders_url_positions_without_a_span(): extracted = ExtractedClaims.model_validate( { "status": "ready", - "claim": "A rose 5%.", - "identified_claims": [], - "locations": [{"claim": "A rose 5%.", "positions": [{"start": None, "end": None, "text": "A grew 5 %."}]}], + "claims": [{"claim": "A rose 5%.", "positions": [{"start": None, "end": None, "text": "A grew 5 %."}]}], } ) text = _render_pretty(extracted) @@ -510,7 +506,7 @@ def test_extract_pretty_renders_url_positions_without_a_span(): def test_extract_pretty_without_locations_prints_no_positions(): - text = _render_pretty(ExtractedClaims.model_validate({"status": "ready", "claim": "A rose 5%."})) + text = _render_pretty(ExtractedClaims.model_validate({"status": "ready", "claims": [{"claim": "A rose 5%."}]})) assert " at " not in text @@ -558,20 +554,18 @@ def test_extract_reads_stdin(monkeypatch): def test_extract_pretty_renders_atomic_claim(): - """Single-claim input fills atomic_claim, not identified_claims — the pretty - renderer must surface it (regression: it used to print 'no claims found').""" + """A single claim comes back as a list of one — the pretty renderer must + surface it (regression: it used to print 'no claims found').""" import io from rich.console import Console from lenz_io.cli.render import Output, render_extract - # The public /extract response carries the primary claim under `claim` - # (the server renames framing's internal `atomic_claim` → `claim`). extracted = ExtractedClaims.model_validate( { "domain": "History", - "claim": "Einstein won the 1921 Nobel Prize in Physics.", + "claims": [{"claim": "Einstein won the 1921 Nobel Prize in Physics."}], "key_entities": [{"name": "Albert Einstein", "type": "person"}], } ) @@ -586,9 +580,9 @@ def test_extract_pretty_renders_atomic_claim(): def test_extract_pretty_multi_claim_includes_primary(): - """The server puts the primary claim in atomic_claim and extras in - identified_claims — the rendered list must include BOTH (regression: the - primary used to be dropped from the list and only shown in the verify hint).""" + """Every claim in ``claims`` is rendered, the first included (regression: + the primary used to be dropped from the list and only shown in the verify + hint).""" import io from rich.console import Console @@ -598,8 +592,7 @@ def test_extract_pretty_multi_claim_includes_primary(): extracted = ExtractedClaims.model_validate( { "domain": "Science", - "claim": "The Earth is flat.", - "identified_claims": ["Ruby is harder than diamond."], + "claims": [{"claim": "The Earth is flat."}, {"claim": "Ruby is harder than diamond."}], } ) buf = io.StringIO() @@ -741,31 +734,50 @@ def test_render_assess_list_rows_show_hint_and_also_found(): out = _render( render_assess, - AssessResponse( - claims=[ - AssessClaim(claim="Water boils at 100 °C at sea level.", verdict="True", confidence="high"), - AssessClaim( - claim="Bilingual children develop stronger executive function.", - verdict="Mixed", - confidence="medium", - identified_claims=["Bilingual children learn to read later than monolingual peers."], - hint="Assessed the main claim only. Send identified_claims as their own items to check the rest.", - ), - AssessClaim( - claim="this is fine", - verdict="Error", - confidence="low", - error_code="no_claim", - hint="No factual statement that can be checked against evidence was found in the input.", - ), - AssessClaim( - claim="The Eiffel Tower is 330 metres tall.", - verdict="Error", - confidence="low", - error_code="timeout", - hint="This item was not processed inside the call's time budget; nothing was charged.", - ), - ] + AssessResponse.model_validate( + { + "status": "ok", + "claims": [ + { + "claim": "Water boils at 100 °C at sea level.", + "status": "completed", + "verdict": "True", + "confidence": "high", + "failure": None, + "more_claims": [], + }, + { + "claim": "Bilingual children develop stronger executive function.", + "status": "completed", + "verdict": "Mixed", + "confidence": "medium", + "failure": None, + "more_claims": ["Bilingual children learn to read later than monolingual peers."], + }, + { + "claim": "this is fine", + "status": "failed", + "verdict": None, + "confidence": None, + "failure": { + "code": "no_checkable_claim", + "hint": "No factual statement that can be checked against evidence was found in the input.", + }, + "more_claims": [], + }, + { + "claim": "The Eiffel Tower is 330 metres tall.", + "status": "failed", + "verdict": None, + "confidence": None, + "failure": { + "code": "timeout", + "hint": "This item was not processed inside the call's time budget; nothing was charged.", + }, + "more_claims": [], + }, + ], + } ), ) lines = out.splitlines() @@ -894,7 +906,7 @@ def _pool_usage(**overrides): payload = { "plan": "pro", "quota_resets_at": "2026-09-01", - "credits": {"total": 5200, "used": 130, "remaining": 5070, "bonus": 200, "resets_at": "2026-09-01"}, + "credits": {"total": 5200, "used": 130, "remaining": 5070, "extra": 200, "resets_at": "2026-09-01"}, "costs": {"verify": 10, "assess": 1, "ask": 1, "extract": 0}, "cost_options": {"verify": {"depth": {"standard": 10, "low": 5}}}, "verify": {"quota_used": 13, "quota_total": 520, "quota_remaining": 507, "bonus": 20, "remaining": 507}, @@ -959,8 +971,8 @@ def test_usage_pretty_leads_with_the_credit_balance(): assert "1 credit each" in text # assess/ask are the unit — singular assert text.index("credits left") < text.index("Verify:") assert text.index("Verify:") < text.index("Ask:") < text.index("Assess:") < text.index("Extract:") - # No extra-credit tail on Ask (its bonus == 0 there) - assert "extra" not in text.split("Ask:")[1].split("Assess:")[0] + # The extra credits show on every row, in that capability's unit. + assert "+ 200 extra" in text.split("Ask:")[1].split("Assess:")[0] assert "4 / 1000 today" in text # Humanized: absolute date always present; the relative prefix ("in N days") # is wall-clock-dependent so isn't asserted here (see _humanize_reset unit test). @@ -1028,23 +1040,12 @@ def test_usage_pretty_never_reads_the_deprecated_alias(): assert "20 extra" in text -def test_usage_pretty_pre_pool_server_has_no_balance_headline(): - """A server predating the pool sends no `credits` block. Print the rows it - did send rather than a confident '0 credits left'.""" - text = _render_usage_text( - _usage( - plan="plus", - quota_resets_at="2026-09-01", - verify={"quota_used": 5, "quota_total": 100, "quota_remaining": 95, "credits": 0, "remaining": 95}, - ask={"quota_used": 0, "quota_total": 50, "quota_remaining": 50, "remaining": 50}, - assess={"quota_used": 0, "quota_total": 500, "quota_remaining": 500, "remaining": 500}, - extract={"calls_today": 0, "daily_limit": 1000}, - ) - ) +def test_usage_pretty_without_a_pool_prints_no_balance_or_rows(): + """A body with no `credits` block: print the plan, not a confident '0 credits left'.""" + text = _render_usage_text(_usage(plan="plus", extract={"calls_today": 0, "daily_limit": 1000})) assert "credits left" not in text - assert "95 left" in text - assert "credits each" not in text # no price list either - assert "Credits reset" in text # falls back to quota_resets_at + assert "Verify:" not in text + assert "plus plan" in text and "0 / 1000 today" in text @pytest.mark.parametrize( diff --git a/tests/test_current_api_version.py b/tests/test_current_api_version.py index bd6a2dd..c9bfda7 100644 --- a/tests/test_current_api_version.py +++ b/tests/test_current_api_version.py @@ -221,10 +221,7 @@ def test_a_receipt_without_chain_id_reads_it_as_empty() -> None: from lenz_io.models import TaskAccepted current = TaskAccepted.model_validate(_canonical("verify__submit_202.json")["body"]) - original = TaskAccepted.model_validate(load("legacy", "verify__submit_202.json")["body"]) assert current.chain_id == "" - assert original.chain_id == load("legacy", "verify__submit_202.json")["body"]["chain_id"] - assert "chain_id" in original.model_dump() @pytest.mark.parametrize("blank", ["", " ", "\t\n", None]) @@ -309,3 +306,18 @@ def test_a_batch_item_language_error_names_its_item() -> None: assert err.message == f"claims[1].{sentence}" assert err.code == "" assert err.errors == [] + + +def test_deprecated_property_aliases_are_marked_without_warning() -> None: + """PEP 702 markers (editors and type checkers see them); no runtime warning, + so code under ``-W error`` keeps working.""" + import warnings + + from lenz_io.errors import LenzQuotaExceededError + from lenz_io.models import TaskAccepted + + for prop in (TaskAccepted.chain_id, LenzQuotaExceededError.credits_remaining): + assert isinstance(prop, property) and isinstance(getattr(prop.fget, "__deprecated__", None), str) + with warnings.catch_warnings(): + warnings.simplefilter("error") + assert TaskAccepted.model_validate({"task_id": "t"}).chain_id == "" diff --git a/tests/test_parity.py b/tests/test_parity.py index 45315a1..7e549b8 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -1,22 +1,21 @@ -"""Both response shapes give existing code exactly what the previous release did. +"""The current response shape gives existing code exactly what 2.x did. -The oracle in ``tests/fixtures/parity/expected/`` was produced by the code -BEFORE this SDK read the newer response shape (see -``tests/parity_generate.py``), from the original-shape responses in -``tests/fixtures/parity/legacy/``. Each response has its newer-shape twin in -``tests/fixtures/parity/canonical/`` (the same answer, as the API's newer +The oracle in ``tests/fixtures/parity/expected/`` was produced by the 2.x code +(see ``tests/parity_generate.py``), from the 2.x-shape responses in +``tests/fixtures/parity/legacy/``. Each response has its current-shape twin in +``tests/fixtures/parity/canonical/`` (the same answer, as the API's current version sends it). -* An original-shape response must produce the frozen output byte for byte: - the model dump, every CLI rendering (text and ``--json``), the exception - ``wait`` raises, the error an HTTP error maps to, the parsed webhook. -* Its newer-shape twin must produce the same values through every attribute - the previous release had, the same CLI text, the same ``wait`` outcome and - the same error, except for the few values the newer shape does not carry, +* A 2.x-shape WEBHOOK payload must produce the frozen output byte for byte: + the parsed event. (The SDK's own calls no longer read the 2.x response + shape; webhooks of work submitted by older clients still arrive in it.) +* A current-shape response must produce the same values through every + attribute 2.x had, the same CLI text, the same ``wait`` outcome and the + same error, except for the few values the current shape does not carry, listed one by one in ``KNOWN_GAPS`` with the reason. ``model_dump()`` and the CLI's ``--json`` show the shape the server sent, so -they are compared for the original shape only. +they are not compared for the current shape. """ from __future__ import annotations @@ -149,8 +148,8 @@ def _old_event_attrs(observed: dict[str, Any], expected: dict[str, Any]) -> dict return {**observed, "event": {k: v for k, v in event.items() if k in expected["event"]}} -@pytest.mark.parametrize("name", names()) -def test_original_shape_is_unchanged(name: str) -> None: +@pytest.mark.parametrize("name", [n for n in names() if n.startswith("webhook__")]) +def test_original_shape_webhook_is_unchanged(name: str) -> None: expected = _expected(name) assert _old_event_attrs(observe(name, load("legacy", name)), expected) == expected diff --git a/tests/test_read_both_shapes.py b/tests/test_read_both_shapes.py index f26b9e4..4af74ba 100644 --- a/tests/test_read_both_shapes.py +++ b/tests/test_read_both_shapes.py @@ -1,6 +1,9 @@ -"""The new attributes read the same from either response shape, and the old -ones keep their meaning. ``test_parity.py`` holds the old attributes to the -previous release's output; this file checks the new ones.""" +"""The current response shape gives the new attributes, and the 2.x names keep +their 2.x meaning. ``test_parity.py`` holds the 2.x attributes to the previous +release's output; this file checks the new ones. The SDK's own calls read only +the current shape; webhook payloads of both shapes are still parsed (the +webhook tests below), and the models they share with the polled bodies read +both.""" from __future__ import annotations @@ -40,9 +43,14 @@ def _both(name: str) -> tuple[dict, dict]: + """The recorded body in the 2.x shape and in the current shape.""" return load("legacy", name)["body"], load("canonical", name)["body"] +def _canonical(name: str) -> dict: + return load("canonical", name)["body"] + + # ── /extract ── @@ -58,21 +66,21 @@ def _both(name: str) -> tuple[dict, dict]: "extract__not_a_claim.json", ], ) -def test_extract_claims_reads_the_same_from_both_shapes(name): - legacy, canonical = (ExtractedClaims.model_validate(b) for b in _both(name)) - assert [c.model_dump() for c in legacy.claims] == [c.model_dump() for c in canonical.claims] - assert legacy.status == canonical.status # "not_a_claim" from either spelling +def test_extract_claims_and_the_2x_names(name): + legacy_body, body = _both(name) + out = ExtractedClaims.model_validate(body) + names = [c["claim"] for c in body["claims"]] + assert [c.claim for c in out.claims] == names + # the 2.x names: ``claim`` is the first, ``identified_claims`` the list when several + assert out.claim == legacy_body["claim"] + assert out.identified_claims == legacy_body["identified_claims"] + assert out.status == legacy_body["status"] # "not_a_claim" in the 2.x spelling def test_extract_one_claim_is_a_list_of_one(): - legacy, canonical = (ExtractedClaims.model_validate(b) for b in _both("extract__ready_one_claim.json")) - assert [c.claim for c in legacy.claims] == [c.claim for c in canonical.claims] == ["Alpha rose 5% in 2024."] - assert canonical.identified_claims == [] and canonical.claim == "Alpha rose 5% in 2024." - - -def test_extract_claims_union_keeps_the_primary_from_an_older_server(): - out = ExtractedClaims.model_validate({"status": "ready", "claim": "A.", "identified_claims": ["B.", "C."]}) - assert [c.claim for c in out.claims] == ["A.", "B.", "C."] + out = ExtractedClaims.model_validate(_canonical("extract__ready_one_claim.json")) + assert [c.claim for c in out.claims] == ["Alpha rose 5% in 2024."] + assert out.identified_claims == [] and out.claim == "Alpha rose 5% in 2024." def test_extract_empty_claims_does_not_fall_back_to_old_fields(): @@ -83,66 +91,47 @@ def test_extract_empty_claims_does_not_fall_back_to_old_fields(): # ── /assess ── -def test_assess_failed_rows_from_both_shapes(): - legacy, canonical = (AssessResponse.model_validate(b) for b in _both("assess__list_mixed_rows.json")) - for resp in (legacy, canonical): - assert resp.status == "ok" - assert [r.status for r in resp.claims] == ["completed", "failed", "failed", "failed"] - assert [r.failure.code if r.failure else None for r in resp.claims] == [ - None, - "no_checkable_claim", - "upstream_unavailable", - "framing_failed", - ] - assert resp.claims[1].failure.hint.startswith("The input is a greeting.") - # the newer shape: no verdict on a failed row, read as the original "Error"/"low" - row = canonical.claims[1] +def test_assess_failed_rows(): + legacy_body, body = _both("assess__list_mixed_rows.json") + resp = AssessResponse.model_validate(body) + assert resp.status == "ok" + assert [r.status for r in resp.claims] == ["completed", "failed", "failed", "failed"] + assert [r.failure.code if r.failure else None for r in resp.claims] == [ + None, + "no_checkable_claim", + "upstream_unavailable", + "framing_failed", + ] + assert resp.claims[1].failure.hint.startswith("The input is a greeting.") + # the 2.x names: no verdict on a failed row reads as "Error" / "low" + row = resp.claims[1] assert (row.verdict, row.confidence, row.error_code) == ("Error", "low", "no_claim") + assert [r.error_code for r in resp.claims] == [r.get("error_code") for r in legacy_body["claims"]] -def test_assess_single_no_claim_from_both_shapes(): - legacy, canonical = (AssessResponse.model_validate(b) for b in _both("assess__single_no_claim.json")) - for resp in (legacy, canonical): - assert resp.status == "no_checkable_claim" - assert resp.failure is not None and resp.failure.code == "no_checkable_claim" - assert resp.error_code == "no_claim" and resp.error +def test_assess_single_no_claim(): + resp = AssessResponse.model_validate(_canonical("assess__single_no_claim.json")) + assert resp.status == "no_checkable_claim" + assert resp.failure is not None and resp.failure.code == "no_checkable_claim" + assert resp.error_code == "no_claim" and resp.error -def test_assess_more_claims_on_a_row_from_both_shapes(): - legacy, canonical = (AssessResponse.model_validate(b) for b in _both("assess__list_compound_item.json")) - assert legacy.claims[0].more_claims == canonical.claims[0].more_claims == ["Second claim.", "Third claim."] - assert canonical.claims[0].identified_claims == ["Second claim.", "Third claim."] +def test_assess_more_claims_on_a_row(): + resp = AssessResponse.model_validate(_canonical("assess__list_compound_item.json")) + assert resp.claims[0].more_claims == resp.claims[0].identified_claims == ["Second claim.", "Third claim."] def test_assess_empty_more_claims_does_not_fall_back(): row = AssessClaim.model_validate({"claim": "x", "status": "completed", "verdict": "True", "more_claims": []}) assert row.more_claims == [] and row.identified_claims == [] - row = AssessClaim.model_validate( - {"claim": "x", "verdict": "True", "more_claims": [], "identified_claims": ["stale"]} - ) - assert row.more_claims == [] - - -@pytest.mark.parametrize( - ("rows", "status"), - [ - ([{"verdict": "Error", "error_code": "no_claim"}] * 2, "no_checkable_claim"), - ([{"verdict": "Error", "error_code": "timeout"}], "error"), - ([{"verdict": "Error", "error_code": "timeout"}, {"verdict": "True"}], "ok"), - ], -) -def test_assess_status_is_computed_for_the_original_shape(rows, status): - assert AssessResponse.model_validate({"claims": rows, "error": None}).status == status # ── /verify ── -def test_receipt_claim_from_both_shapes(): - legacy, canonical = (BatchAccepted.model_validate(b) for b in _both("verify__batch_202.json")) - assert ( - [i.claim for i in legacy.items] == [i.claim for i in canonical.items] == [i.claim_text for i in canonical.items] - ) +def test_receipt_claim(): + out = BatchAccepted.model_validate(_canonical("verify__batch_202.json")) + assert [i.claim for i in out.items] == [i.claim_text for i in out.items] != [] assert TaskAccepted.model_validate({"task_id": "t"}).claim == "" @@ -155,24 +144,25 @@ def test_receipt_claim_from_both_shapes(): "verify__status_cancelled_after_a_while.json", ], ) -def test_failed_status_failure_block_from_both_shapes(name): - legacy, canonical = (TaskStatus.model_validate(b) for b in _both(name)) - for st in (legacy, canonical): - assert st.failure is not None - assert st.failure.code == _both(name)[1]["failure"]["code"] - assert st.failure.failure_class == canonical.failure_class - assert st.failure.retryable is canonical.retryable - assert st.failure.detail - assert canonical.failure_reason == legacy.failure_reason - # ``error`` keeps the original sentence, rebuilt from the code (a failure +def test_failed_status_failure_block(name): + legacy_body, body = _both(name) + canonical = TaskStatus.model_validate(body) + assert canonical.failure is not None + assert canonical.failure.code == body["failure"]["code"] + assert canonical.failure.failure_class == canonical.failure_class + assert canonical.failure.retryable is canonical.retryable + assert canonical.failure.detail + assert canonical.failure_reason == legacy_body["failure_reason"] + # ``error`` keeps the 2.x sentence, rebuilt from the code (a failure # read back from storage said "Pipeline stopped: ." instead). - if not legacy.error.startswith("Pipeline stopped: "): - assert canonical.error == legacy.error + if not legacy_body["error"].startswith("Pipeline stopped: "): + assert canonical.error == legacy_body["error"] -def test_needs_input_options_from_both_shapes(): - legacy, canonical = (TaskStatus.model_validate(b) for b in _both("verify__status_needs_input.json")) - assert [c.claim for c in legacy.claims] == [c.claim for c in canonical.claims] +def test_needs_input_options(): + legacy_body, body = _both("verify__status_needs_input.json") + canonical = TaskStatus.model_validate(body) + assert [c.text for c in canonical.claims] == [c["text"] for c in legacy_body["claims"]] assert [c.text for c in canonical.claims] == [c.claim for c in canonical.claims] @@ -187,9 +177,7 @@ def test_completed_at_and_the_original_modified_at_rule(): {"created_at": "2026-10-01T22:00:00+00:00", "completed_at": "2026-10-02T01:00:00+02:00"} ) assert offset.modified_at is None - legacy = Verification.model_validate({"created_at": late, "modified_at": early}) - assert legacy.completed_at == early - assert Verification.model_validate({"created_at": late, "modified_at": None}).completed_at is None + assert Verification.model_validate({"created_at": late}).completed_at is None @pytest.mark.parametrize( @@ -200,11 +188,13 @@ def test_completed_at_and_the_original_modified_at_rule(): "verify__list_200_modified_at_crosses_midnight_by_minutes.json", ], ) -def test_modified_at_from_both_shapes(name): - cls = VerificationList if "list" in name else Verification - legacy, canonical = (cls.model_validate(b) for b in _both(name)) - pick = (lambda m: m.items[0]) if cls is VerificationList else (lambda m: m) - assert pick(legacy).modified_at == pick(canonical).modified_at +def test_modified_at_follows_the_2x_rule(name): + legacy_body, body = _both(name) + if "list" in name: + got, expected = VerificationList.model_validate(body).items[0], legacy_body["items"][0] + else: + got, expected = Verification.model_validate(body), legacy_body + assert got.modified_at == expected["modified_at"] # ── /me/usage ── @@ -215,13 +205,17 @@ def test_modified_at_from_both_shapes(name): ["account__me_usage_pro_extra.json", "account__me_usage_free_partly_spent.json", "account__me_usage_free.json"], ) def test_usage_blocks_are_computed_from_the_pool(name): - legacy, canonical = (Usage.model_validate(b) for b in _both(name)) + legacy_body, body = _both(name) + canonical = Usage.model_validate(body) for cap in ("verify", "ask", "assess"): - assert getattr(legacy, cap).model_dump() == getattr(canonical, cap).model_dump() - assert canonical.quota_resets_at == canonical.credits.resets_at + dumped = getattr(canonical, cap).model_dump() + assert {k: dumped[k] for k in legacy_body[cap] if k != "credits"} == { + k: v for k, v in legacy_body[cap].items() if k != "credits" + } + assert canonical.quota_resets_at == canonical.credits.resets_at == legacy_body["quota_resets_at"] with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) - assert canonical.credits.bonus == legacy.credits.bonus + assert canonical.credits.bonus == legacy_body["credits"]["bonus"] # ── /review ── @@ -256,7 +250,7 @@ def test_review_failure_code_reads_the_new_spelling(): assert FailureBlock.model_validate({"failure_reason": "not_a_claim"}).code == "no_checkable_claim" -# ── the original shape is parsed exactly as before ── +# ── the models keep their 2.x layout ── def test_newer_shape_dump_keeps_every_key_sent(): @@ -276,12 +270,6 @@ def test_sparse_usage_dumps_every_default(): assert set(Usage.model_validate({"plan": "free"}).model_dump()) == set(Usage.model_fields) -def test_original_shape_keeps_unset_fields_unset(): - body = {"claim": "x", "verdict": "True", "confidence": "high"} - assert AssessClaim.model_validate(body).model_dump(exclude_unset=True) == body - assert AssessResponse.model_validate({"claims": []}).model_dump(exclude_unset=True) == {"claims": []} - - def test_new_names_are_properties_not_fields(): for cls, name in ( (AssessClaim, "status"), @@ -380,19 +368,16 @@ def test_409_failed_run_reads_the_failure_block(): assert map_response_to_error(409, json.dumps(canonical).encode()).failure_reason == "not_a_claim" -def test_validation_errors_from_both_shapes(): - legacy, canonical = _both("errors__validation_wrong_type.json") - for body in (legacy, canonical): - err = map_response_to_error(422, json.dumps(body).encode()) - assert isinstance(err, LenzValidationError) - assert [e["msg"] for e in err.errors] == ["Input should be a valid string"] +def test_validation_errors(): + err = map_response_to_error(422, json.dumps(_canonical("errors__validation_wrong_type.json")).encode()) + assert isinstance(err, LenzValidationError) + assert [e["msg"] for e in err.errors] == ["Input should be a valid string"] -def test_rate_limit_wait_from_both_shapes(): - for body in _both("errors__rate_limited_extract.json"): - err = map_response_to_error(429, json.dumps(body).encode()) - assert isinstance(err, LenzRateLimitError) - assert (err.retry_after, err.reset_in_seconds) == (900, 900) +def test_rate_limit_wait(): + err = map_response_to_error(429, json.dumps(_canonical("errors__rate_limited_extract.json")).encode()) + assert isinstance(err, LenzRateLimitError) + assert (err.retry_after, err.reset_in_seconds) == (900, 900) err = map_response_to_error(429, json.dumps({"code": "review_in_flight", "retry_after": 0}).encode()) assert (err.retry_after, err.reset_in_seconds) == (0, None) @@ -407,10 +392,8 @@ def test_error_fields_follow_key_presence(): def test_quota_remaining_is_derived_when_absent(): - legacy, canonical = _both("citecheck__402_no_credits.json") + canonical = _canonical("citecheck__402_no_credits.json") assert map_response_to_error(402, json.dumps(canonical).encode()).remaining == 100 - assert map_response_to_error(402, json.dumps(legacy).encode()).remaining == 100 - assert map_response_to_error(402, json.dumps({"credits_remaining": 9, "cost": 1}).encode()).remaining is None # ── requests ── diff --git a/tests/test_usage_models.py b/tests/test_usage_models.py index b54604e..41fb564 100644 --- a/tests/test_usage_models.py +++ b/tests/test_usage_models.py @@ -133,16 +133,6 @@ def test_dumping_keeps_the_alias_and_does_not_warn(): assert dumped["credits"] == 20 -def test_old_server_sending_only_credits_still_fills_bonus(): - """Pre-pool server (or a mid-deploy revision): `credits` only.""" - cap = UsageCapacity.model_validate( - {"quota_used": 120, "quota_total": 500, "quota_remaining": 380, "credits": 25, "remaining": 405} - ) - assert cap.bonus == 25 - with pytest.deprecated_call(): - assert cap.credits == 25 - - def test_server_after_the_alias_removal_still_fills_credits(): """A response without `credits` (a computed block) still reads the alias.""" cap = UsageCapacity.model_validate( @@ -187,11 +177,6 @@ def test_dumping_the_pool_keeps_bonus_and_does_not_warn(): assert dumped["extra"] == dumped["bonus"] == 200 -def test_a_server_sending_only_bonus_still_fills_extra(): - c = UsageCredits.model_validate({"total": 300, "used": 0, "remaining": 300, "bonus": 200}) - assert c.extra == 200 - - def test_a_server_sending_only_extra_still_fills_bonus(): """The newer response shape sends only `extra`; `bonus` keeps reading.""" c = UsageCredits.model_validate({"total": 300, "used": 0, "remaining": 300, "extra": 200}) diff --git a/uv.lock b/uv.lock index 998c139..013068c 100644 --- a/uv.lock +++ b/uv.lock @@ -414,6 +414,8 @@ source = { editable = "." } dependencies = [ { name = "httpx" }, { name = "pydantic" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" }, + { name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" }, ] [package.optional-dependencies] @@ -454,6 +456,7 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, { name = "typer", marker = "extra == 'cli'", specifier = ">=0.12" }, { name = "typer", marker = "extra == 'dev'", specifier = ">=0.12" }, + { name = "typing-extensions", specifier = ">=4.5" }, ] provides-extras = ["cli", "dev"] From ee617717dafa41e207b84d24246479fee758ddbf Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 12:52:08 +0300 Subject: [PATCH 12/67] fix(errors): credit_balance is restored for the citation check only The current shape leaves the pool out of a citation check's 402 because it equals remaining; the endpoint-aware conversion already restores it for that call. Drop the generic fallback that invented a pool figure on every other 402. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/errors.py | 7 +++---- tests/test_current_api_version.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index fe8b483..0166c94 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -794,11 +794,10 @@ def _read(key: str, block_key: str) -> Any: # credits. The body's ``credits_remaining`` lands on ``credit_balance``, # NOT on the same-named deprecated property — that one aliases # ``remaining`` and means a different quantity (see the class docstring). + # (A citation check's current-shape 402 leaves the pool out because it + # equals ``remaining``; ``_original_error`` restores it for that + # endpoint only.) err.credit_balance = _opt_int(parsed.get("credits_remaining")) - if err.credit_balance is None and err.remaining is not None and "remaining" in parsed: - # The current shape leaves the pool out where it equals - # ``remaining`` (a citation check costs one credit). - err.credit_balance = err.remaining err.cost = _opt_int(parsed.get("cost")) resets_at = parsed.get("resets_at") err.resets_at = resets_at if isinstance(resets_at, str) and resets_at else None diff --git a/tests/test_current_api_version.py b/tests/test_current_api_version.py index c9bfda7..5f8382b 100644 --- a/tests/test_current_api_version.py +++ b/tests/test_current_api_version.py @@ -321,3 +321,31 @@ def test_deprecated_property_aliases_are_marked_without_warning() -> None: with warnings.catch_warnings(): warnings.simplefilter("error") assert TaskAccepted.model_validate({"task_id": "t"}).chain_id == "" + + +# ── ``credit_balance`` is restored for the citation check only ── + + +def _no_credits(path: str, body: dict[str, Any], client: Lenz) -> LenzQuotaExceededError: + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.post(path).respond(402, json=body) + with pytest.raises(LenzQuotaExceededError) as info: + if path == "/citecheck": + client.citecheck(text="See https://example.org/a.") + else: + client.review("A claim.") + return info.value + + +def test_a_citecheck_402_without_the_pool_reads_it_as_remaining(client: Lenz) -> None: + body = _canonical("citecheck__402_no_credits.json")["body"] + assert "credits_remaining" not in body + assert _no_credits("/citecheck", body, client).credit_balance == body["remaining"] + + +def test_another_endpoint_does_not_invent_a_credit_balance(client: Lenz) -> None: + body = {"detail": "No remaining credits.", "code": "no_credits", "remaining": 7, "cost": 1} + assert _no_credits("/review", body, client).credit_balance is None + from lenz_io.errors import map_response_to_error + + assert map_response_to_error(402, json.dumps(body)).credit_balance is None # type: ignore[attr-defined] From 9e6004db40ce2a6be8bd3dd1e2856ad938b93a91 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 12:52:24 +0300 Subject: [PATCH 13/67] feat: the version header goes on every request; an answer in another version raises LenzApiVersionError The header is now set per request, so an http_client= with no header or a stale default still asks for 2026-10-11. A response (success or error) whose X-Lenz-API-Version names another version raises LenzApiVersionError, carrying status_code, the body as sent and api_version, instead of being misread. A missing header proceeds; webhook parsing is not guarded; batch waits re-raise it rather than treating it as a failed poll. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/__init__.py | 2 + src/lenz_io/client.py | 38 +++++++++ src/lenz_io/errors.py | 24 ++++++ tests/test_api_version_guard.py | 134 ++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 tests/test_api_version_guard.py diff --git a/src/lenz_io/__init__.py b/src/lenz_io/__init__.py index 3fe51ec..23b4d9d 100644 --- a/src/lenz_io/__init__.py +++ b/src/lenz_io/__init__.py @@ -52,6 +52,7 @@ CitecheckFailed, CitecheckTimeout, LenzAPIError, + LenzApiVersionError, LenzAuthError, LenzError, LenzGoneError, @@ -176,6 +177,7 @@ "FailureClass", "Lenz", "LenzAPIError", + "LenzApiVersionError", "LenzAuthError", "LenzError", "LenzGoneError", diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index a5fb01f..cd7fe63 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -93,6 +93,7 @@ CitecheckFailed, CitecheckTimeout, LenzAPIError, + LenzApiVersionError, LenzError, LenzGoneError, LenzNeedsInputError, @@ -136,6 +137,7 @@ # had in 2.x; the old names are deprecated aliases. Webhooks are the one # place both shapes are still parsed. API_VERSION = "2026-10-11" +_VERSION_HEADER = "X-Lenz-API-Version" DEFAULT_BASE_URL = "https://lenz.io/api/v1" DEFAULT_TIMEOUT = 30.0 @@ -1501,6 +1503,9 @@ def _poll_to_terminal( # that is still running. gone[task_id] = exc continue + except LenzApiVersionError: + # Not a failed poll: every later poll would answer the same. + raise except LenzError: # Don't let one id's poll failure abort the rest — retry it # next round (bounded by the deadline below). @@ -1789,6 +1794,9 @@ def _request( if self._api_key and (auth_required or auth_optional): req_headers["Authorization"] = f"Bearer {self._api_key}" req_headers.setdefault("Content-Type", "application/json") + # The version is part of the request, not of the HTTP client: a client + # passed as ``http_client=`` may carry no version header or a stale one. + req_headers[_VERSION_HEADER] = API_VERSION # A per-call ``timeout`` overrides the client-wide one for this request # only; ``None`` keeps httpx on the client default. req_timeout = httpx.USE_CLIENT_DEFAULT if timeout is None else httpx.Timeout(timeout) @@ -1811,6 +1819,8 @@ def _request( time.sleep(_retry_sleep(attempt)) continue + _check_served_version(response) + if response.status_code < 400: return response.json() if response.content else {} @@ -1929,6 +1939,34 @@ def _review_failed(review: ReviewFull) -> ReviewFailed: ) +def _check_served_version(response: httpx.Response) -> None: + """Refuse an answer in an API version this SDK does not read. + + A response without the header proceeds (a proxy or an old server may not + send it). Webhook payloads never pass through here. + """ + served = (response.headers.get(_VERSION_HEADER) or "").strip() + if not served or served == API_VERSION: + return + try: + parsed = response.json() if response.content else None + except ValueError: + parsed = None + raise LenzApiVersionError( + message=( + f"The API answered in version {served}; lenz-io 3.x reads {API_VERSION} only. " + f"Change the API version setting on the server side to {API_VERSION}, or pin lenz-io<3." + ), + cause=f"The response carries {_VERSION_HEADER}: {served}.", + fix=(f"Set the account's API version to {API_VERSION}, or install lenz-io<3 to keep reading {served}."), + doc_url="https://lenz.io/docs/errors", + request_id=response.headers.get("X-Request-ID") or "", + status_code=response.status_code, + body=parsed if isinstance(parsed, dict) else None, + api_version=served, + ) + + def _stated_retry_after(response: httpx.Response) -> int | None: """Seconds the server says to wait, or None if it didn't say. diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 0166c94..b383cb4 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -402,6 +402,29 @@ class LenzWebhookSignatureError(LenzError): """ +class LenzApiVersionError(LenzError): + """A response named an API version this SDK does not read. + + Every API response names the version that served it in the + ``X-Lenz-API-Version`` header. lenz-io 3.x asks for ``2026-10-11`` and reads + that version's response shape only, so an answer in another version (in + practice ``2026-05-13``, which a reply replayed from before the account's + version changed, or a server pinned to the older version, still sends) is + refused instead of being misread. Applies to every response of a client + call, success or error; never to webhook payloads. + + Fields: + * ``api_version`` — the version the response named. + * ``status_code`` — the response's HTTP status. + * ``body`` — the response body as sent (parsed JSON), or ``None`` + when it was not a JSON object. + """ + + def __init__(self, *, api_version: str = "", **kwargs: Any) -> None: + super().__init__(api_version=api_version, **kwargs) + self.api_version = api_version + + # ── Mapping table ──────────────────────────────────────────────────────── # # Single source of truth for HTTP status -> exception class + default @@ -894,6 +917,7 @@ def _fix_hint_for(status_code: int) -> str: "CitecheckFailed", "CitecheckTimeout", "LenzAPIError", + "LenzApiVersionError", "LenzAuthError", "LenzError", "LenzGoneError", diff --git a/tests/test_api_version_guard.py b/tests/test_api_version_guard.py new file mode 100644 index 0000000..1dc5715 --- /dev/null +++ b/tests/test_api_version_guard.py @@ -0,0 +1,134 @@ +"""The version header goes on every request, and an answer in another version +is refused with a typed error instead of being misread.""" + +from __future__ import annotations + +import json +from typing import Any + +import httpx +import pytest +import respx +from parity_observe import load + +import lenz_io +from lenz_io import Lenz, LenzApiVersionError, LenzError +from lenz_io.client import API_VERSION, DEFAULT_BASE_URL +from lenz_io.webhooks import parse_webhook + +KEY = "lenz_" + "0" * 32 +OLD = "2026-05-13" + + +def _seen_versions(headers_on_client: dict[str, str] | None) -> list[str | None]: + seen: list[str | None] = [] + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request.headers.get("X-Lenz-API-Version")) + return httpx.Response(200, json={"api": "ok"}) + + http = httpx.Client(transport=httpx.MockTransport(handler), headers=headers_on_client or {}) + with Lenz(api_key=KEY, http_client=http) as c: + c._request("GET", "/") + c._request("POST", "/verify", json={"claim": "x"}, headers={"X-Lenz-API-Version": OLD}) + return seen + + +def test_a_custom_http_client_without_the_header_still_sends_it() -> None: + assert _seen_versions(None) == [API_VERSION, API_VERSION] + + +def test_a_custom_http_client_with_a_stale_default_is_overridden() -> None: + assert _seen_versions({"X-Lenz-API-Version": OLD}) == [API_VERSION, API_VERSION] + + +def test_the_default_client_sends_it() -> None: + with Lenz(api_key=KEY, max_retries=0) as c, respx.mock(base_url=DEFAULT_BASE_URL) as mock: + route = mock.get("/me/usage").respond(200, json={}) + c._request("GET", "/me/usage") + assert route.calls.last.request.headers["X-Lenz-API-Version"] == "2026-10-11" + + +# ── The answer's version ── + + +@pytest.fixture +def client() -> Any: + c = Lenz(api_key=KEY, max_retries=0) + yield c + c.close() + + +def test_an_answer_in_another_version_raises(client: Lenz) -> None: + body = {"status": "ready", "claim": "A.", "identified_claims": []} + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.post("/extract").respond(200, json=body, headers={"X-Lenz-API-Version": OLD}) + with pytest.raises(LenzApiVersionError) as info: + client.extract(text="A.") + exc = info.value + assert isinstance(exc, LenzError) + assert exc.api_version == OLD + assert exc.status_code == 200 + assert exc.body == body + assert OLD in exc.message and "2026-10-11" in exc.message + assert "lenz-io<3" in exc.message + + +def test_an_error_answered_in_another_version_raises_the_version_error(client: Lenz) -> None: + body = {"detail": "No remaining credits.", "code": "no_credits"} + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.post("/verify").respond(402, json=body, headers={"X-Lenz-API-Version": OLD}) + with pytest.raises(LenzApiVersionError) as info: + client.verify("A.") + assert info.value.status_code == 402 + assert info.value.body == body + + +def test_a_non_json_answer_in_another_version_still_raises(client: Lenz) -> None: + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.get("/me/usage").respond(200, content=b"", headers={"X-Lenz-API-Version": OLD}) + with pytest.raises(LenzApiVersionError) as info: + client.usage() + assert info.value.body is None + + +def test_the_current_version_and_a_missing_header_proceed(client: Lenz) -> None: + usage = load("canonical", "account__me_usage_free.json")["body"] + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.get("/me/usage").respond(200, json=usage, headers={"X-Lenz-API-Version": API_VERSION}) + assert client.usage().plan + mock.get("/me/usage").respond(200, json=usage) + assert client.usage().plan + + +def test_a_stored_replay_in_the_old_shape_is_refused(client: Lenz) -> None: + """The API answers a replay of an idempotent request stored by an older + release in the old shape, and says so in the header.""" + fixture = load("legacy", "assess__replay_later_200.json") + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.post("/assess").respond(fixture["status"], json=fixture["body"], headers={"X-Lenz-API-Version": OLD}) + with pytest.raises(LenzApiVersionError) as info: + client.assess("A.", idempotency_key="k") + assert info.value.api_version == OLD + assert info.value.body == fixture["body"] + + +def test_the_guard_does_not_apply_to_webhook_parsing() -> None: + event = load("legacy", "webhook__verification_completed.json")["body"] + assert parse_webhook(event).event == "verification.completed" + + +def test_the_error_is_exported_and_pickles_its_fields() -> None: + assert "LenzApiVersionError" in lenz_io.__all__ + exc = LenzApiVersionError(message="m", status_code=200, body={"a": 1}, api_version=OLD) + assert (exc.api_version, exc.body, exc.status_code) == (OLD, {"a": 1}, 200) + assert json.dumps(exc.body) + + +def test_waiting_on_a_batch_does_not_swallow_the_version_error(client: Lenz) -> None: + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.get(url__regex=r".*/verify/.*").respond( + 200, json={"status": "processing"}, headers={"X-Lenz-API-Version": OLD} + ) + with pytest.raises(LenzApiVersionError): + client._poll_to_terminal(["a" * 32], 5) From f82fcf5f58ac672c7a8ea8b734efc2bb064013a8 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 12:53:29 +0300 Subject: [PATCH 14/67] fix: a verification's failure block spells nothing checkable not_a_claim, as 2.x did TaskStatus.failure and the failure block of a verification.failed event read failure_reason 'not_a_claim' from a current-shape body. Assessment and review failure blocks keep 'no_claim'; failure.code is no_checkable_claim throughout. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/models.py | 13 +++++++- src/lenz_io/webhooks.py | 3 +- tests/test_failure_spelling.py | 57 ++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/test_failure_spelling.py diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 98284ef..031f610 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -1015,7 +1015,10 @@ def failure(self) -> FailureBlock | None: ``retryable``, ``docs_url`` and ``hint`` on a failure, which are deprecated and kept.""" sent = _sent(self, "failure") - return FailureBlock.model_validate(sent) if isinstance(sent, dict) else None + if not isinstance(sent, dict): + return None + # A verification spelled "nothing checkable" ``not_a_claim``. + return FailureBlock.model_validate(_verification_failure(sent)) class BatchItemResult(_Lax): @@ -1395,6 +1398,14 @@ def detail(self) -> str | None: return sent if isinstance(sent, str) else None +def _verification_failure(failure: Any) -> Any: + """A newer-shape ``failure`` block of a verification, with the original + spelling of "nothing checkable" (``not_a_claim``) in ``failure_reason``.""" + if isinstance(failure, dict) and _is_newer(failure, "code"): + return _fill(failure, failure_reason=_old_code(failure["code"], "not_a_claim")) + return failure + + def _deep_check_failure(data: Any) -> Any: """A newer-shape ``failure`` block of a deep check inside a review: its "nothing checkable" was ``not_a_claim`` (a quick check's and the review's diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index 17ab451..5234d50 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -45,6 +45,7 @@ ReviewFull, _fill_modified_at, _old_code, + _verification_failure, ) SIGNATURE_HEADER = "X-Lenz-Signature" @@ -156,7 +157,7 @@ def failure(self) -> FailureBlock | None: return None block = {"failure_reason": self.error, "failure_class": self.failure_class, "retryable": self.retryable} try: - return FailureBlock.model_validate(block) + return FailureBlock.model_validate(_verification_failure(block)) except ValidationError: return None diff --git a/tests/test_failure_spelling.py b/tests/test_failure_spelling.py new file mode 100644 index 0000000..17a6ed7 --- /dev/null +++ b/tests/test_failure_spelling.py @@ -0,0 +1,57 @@ +"""How "nothing checkable" is spelled in a failure block. + +2.x spelled it ``not_a_claim`` for a verification (``TaskStatus.failure_reason``, +a ``verification.failed`` webhook's ``error``) and ``no_claim`` for an +assessment or a review. ``failure.failure_reason`` keeps each spelling, and +``failure.code`` is the same ``no_checkable_claim`` everywhere. +""" + +from __future__ import annotations + +import warnings + +import pytest +from parity_observe import load + +from lenz_io.models import AssessResponse, ReviewFull, TaskStatus +from lenz_io.webhooks import VerificationFailed, parse_webhook + + +def test_a_verifications_failure_block_spells_it_not_a_claim() -> None: + status = TaskStatus.model_validate(load("canonical", "verify__status_not_a_claim.json")["body"]) + assert status.failure_reason == "not_a_claim" + assert status.failure is not None + assert status.failure.failure_reason == "not_a_claim" + assert status.failure.code == "no_checkable_claim" + assert TaskStatus.model_validate(load("legacy", "verify__status_not_a_claim.json")["body"]).failure_reason == ( + "not_a_claim" + ) + + +@pytest.mark.parametrize("shape", ["legacy", "canonical"]) +def test_a_verification_failed_event_spells_it_not_a_claim(shape: str) -> None: + event = parse_webhook(load(shape, "webhook__verification_failed_not_a_claim.json")["body"]) + assert isinstance(event, VerificationFailed) + assert event.error == "not_a_claim" + assert event.failure is not None + assert event.failure.failure_reason == "not_a_claim" + + +def test_another_failure_code_is_untouched() -> None: + body = load("canonical", "webhook__verification_failed_upstream_unavailable.json")["body"] + event = parse_webhook(body) + assert isinstance(event, VerificationFailed) + assert event.failure is not None + assert event.failure.failure_reason == event.failure.code == body["verification"]["failure"]["code"] + + +def test_an_assessment_and_a_review_keep_no_claim() -> None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + assessed = AssessResponse.model_validate(load("canonical", "assess__single_no_claim.json")["body"]) + assert assessed.failure is not None + assert assessed.failure.failure_reason == "no_claim" + assert assessed.error_code == "no_claim" + review = ReviewFull.model_validate(load("canonical", "review__get_failed_no_claim.json")["body"]) + assert review.failure is not None + assert review.failure.failure_reason == "no_claim" From a01efbb2c326e3dcc18d564446b853337e022b93 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 12:54:04 +0300 Subject: [PATCH 15/67] fix: a completed quick check in a review reads the 2.x compound hint A completed review assessment with other claims found reads the 2.x sentence in hint; a failed one reads None, as before. The parity exemption that hid the difference is removed. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/models.py | 8 +++++++- tests/test_parity.py | 3 --- tests/test_read_both_shapes.py | 13 +++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 031f610..c1949e9 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -619,6 +619,9 @@ def claims(self) -> list[ExtractedClaim]: #: The original shape's ``hint`` on an /assess verdict row that found other #: claims in its input, and its ``error`` on an input holding no claim. +#: A completed quick check inside a review that found other claims: the one +#: sentence 2.x put in ``hint`` (a failed quick check had none). +_REVIEW_COMPOUND_HINT = "This text holds more than one claim." _COMPOUND_HINT = "Assessed the main claim only. Send identified_claims as their own items to check the rest." _NO_CLAIM_ERROR = "No verifiable claim detected" @@ -1595,10 +1598,13 @@ def _read_newer_shape(cls, data: Any) -> Any: if not _is_newer(data, "more_claims"): return data failure = data.get("failure") if isinstance(data.get("failure"), dict) else {} + more = data.get("more_claims") or [] + completed = not failure and data.get("verdict") is not None return _fill( data, - identified_claims=data.get("more_claims") or [], + identified_claims=more, error_code=_old_code(failure.get("code"), "no_claim") if failure else None, + hint=_REVIEW_COMPOUND_HINT if completed and more else None, ) @property diff --git a/tests/test_parity.py b/tests/test_parity.py index 7e549b8..7e0204c 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -101,9 +101,6 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, ("review__delete_not_a_route.json", "error.code"): ( "a route no SDK method calls: the original answered it without a JSON body" ), - ("review__get_assessment_rows_full_fields.json", "dump.claims[0].assessment.hint"): ( - "the newer shape sends no hint on a completed quick check with other claims found" - ), ("review__get_failed_every_assessment_failed.json", "dump.failure.hint"): "the server words the hint differently", ("review__get_failed_every_assessment_failed.json", "render"): "the same hint, printed by the CLI", ("review__get_failed_every_assessment_failed.json", "render_issues"): "the same hint, printed by the CLI", diff --git a/tests/test_read_both_shapes.py b/tests/test_read_both_shapes.py index 4af74ba..f2b8c7a 100644 --- a/tests/test_read_both_shapes.py +++ b/tests/test_read_both_shapes.py @@ -421,3 +421,16 @@ def test_verify_never_sends_an_empty_webhook_url(webhook_url): def test_verify_sends_a_webhook_url_that_was_set(): body = _sent(lambda c: c.verify("x", webhook_url="https://hooks.example.test/x")) assert body["webhook_url"] == "https://hooks.example.test/x" + + +def test_a_completed_compound_quick_check_in_a_review_reads_the_2x_hint() -> None: + from parity_observe import load + + from lenz_io.models import ReviewFull + + review = ReviewFull.model_validate(load("canonical", "review__get_assessment_rows_full_fields.json")["body"]) + done, failed = (c.assessment for c in review.claims[:2]) + assert done is not None and failed is not None + assert done.hint == "This text holds more than one claim." + assert done.more_claims == done.identified_claims + assert failed.hint is None From 102032060a093653efc1af42275210c72162db4d Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 12:55:10 +0300 Subject: [PATCH 16/67] feat(models): mark every deprecated field in the JSON schema The 2.x names kept as fields (claim, identified_claims, locations, error_code, hint, error, failure_*, claim_text, modified_at, *_limit_reached, the Usage capacity blocks and quota_resets_at, and the rest) carry deprecated in their schema, as candidate_claims already did. The schema-compatibility test ignores the marker. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/models.py | 62 +++++++++++++++---------------- tests/test_deprecation_markers.py | 56 ++++++++++++++++++++++++++++ tests/test_parity.py | 5 ++- 3 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 tests/test_deprecation_markers.py diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index c1949e9..9a67dbb 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -203,7 +203,7 @@ class CandidateClaim(_Lax): """One of multiple distinct claims framing found in the submitted text.""" #: **Deprecated**, use :attr:`claim` (the same string). - text: str = "" + text: str = Field(default="", json_schema_extra={"deprecated": True}) domain: str = "" @model_validator(mode="before") @@ -391,7 +391,7 @@ class Verification(_Lax): created_at: str | None = None # Deprecated: use ``completed_at``. Set only when the verification # completed on a later UTC day than ``created_at``. - modified_at: str | None = None + modified_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) # Output language (ISO 639-1). Always populated by the server when # the SDK is fresh; defaulted to ``'en'`` for resilience against # older cached payloads that lack the field. @@ -440,7 +440,7 @@ class VerificationListItem(_Lax): suggested_rewrite: str | None = None created_at: str | None = None # Deprecated: use ``completed_at``. See ``Verification.modified_at``. - modified_at: str | None = None + modified_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) # Output language (ISO 639-1). See ``Verification.language``. language: str = "en" @@ -574,9 +574,9 @@ class ExtractedClaims(_Lax): # ``"not_a_claim"`` is the 2.x spelling of the API's ``no_checkable_claim``. status: str = "" # Deprecated: use ``claims[0].claim``. - claim: str = "" + claim: str = Field(default="", json_schema_extra={"deprecated": True}) # Deprecated: use ``claims`` (this is its names, and ``[]`` for one claim). - identified_claims: list[str] = Field(default_factory=list) + identified_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) # Deprecated: always empty since 2026-09-12. Kept because the server # still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) @@ -585,7 +585,7 @@ class ExtractedClaims(_Lax): presumed_intent: str = "" original_input: str = "" # Deprecated: use ``claims`` (each with its ``positions``). - locations: list[ClaimLocation] | None = None + locations: list[ClaimLocation] | None = Field(default=None, json_schema_extra={"deprecated": True}) @model_validator(mode="before") @classmethod @@ -686,18 +686,18 @@ class AssessClaim(_Lax): # 'framing_failed' is deterministic, so retrying the same text will not # help (a provider outage comes back as 'upstream_unavailable' instead). # 'no_claim' wants a different input; read ``hint``. - error_code: str | None = None + error_code: str | None = Field(default=None, json_schema_extra={"deprecated": True}) # Deprecated: always empty since 2026-09-12, when the ``ambiguous`` cause # that filled it was retired. Kept because the server still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) # Deprecated: use ``more_claims``. Other claims found in the input that # were NOT assessed; else empty. - identified_claims: list[str] = Field(default_factory=list) + identified_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) # Deprecated: use ``failure.hint`` (on a failed row; the sentence on a # verdict row that found other claims has no replacement). One sentence on # what to send next. Set on every Error row and on a row with a non-empty # ``identified_claims``; ``None`` on a plain verdict row. - hint: str | None = None + hint: str | None = Field(default=None, json_schema_extra={"deprecated": True}) @model_validator(mode="before") @classmethod @@ -784,9 +784,9 @@ class AssessResponse(_Lax): claims: list[AssessClaim] = Field(default_factory=list) # Deprecated: use ``failure`` / ``status``. The 2.x sentence for an input # with no claim. - error: str | None = None + error: str | None = Field(default=None, json_schema_extra={"deprecated": True}) # Deprecated: use ``failure.code`` (``"no_checkable_claim"``). - error_code: str = "" # '' | 'no_claim' + error_code: str = Field(default="", json_schema_extra={"deprecated": True}) # '' | 'no_claim' # Deprecated: always empty since 2026-09-12. Kept because the server # still sends the key. candidate_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) @@ -831,7 +831,7 @@ class TaskAccepted(_Lax): task_id: str = "" #: **Deprecated**, use :attr:`claim`. - claim_text: str = "" + claim_text: str = Field(default="", json_schema_extra={"deprecated": True}) @model_validator(mode="before") @classmethod @@ -976,19 +976,19 @@ class TaskStatus(_Lax): # (``failure.detail``, ``.code``, ``.failure_class``, ...). ``error`` keeps # its 2.x sentence; ``failure_detail`` is always empty, with no # replacement. - error: str = "" - failure_reason: str = "" - failure_detail: str = "" + error: str = Field(default="", json_schema_extra={"deprecated": True}) + failure_reason: str = Field(default="", json_schema_extra={"deprecated": True}) + failure_detail: str = Field(default="", json_schema_extra={"deprecated": True}) # WHY it failed — the closed set is ``FailureClass`` (import it for # exhaustive matching). The annotation stays ``str`` on purpose: a # ``Literal`` here would make an unknown class the server adds later a # hard ValidationError, and every other field on this model is lax. # Rows predating 2026-08 omit this and ``retryable`` (the derived retry # signal — true iff ``upstream_unavailable``). - failure_class: str = "" - retryable: bool | None = None + failure_class: str = Field(default="", json_schema_extra={"deprecated": True}) + retryable: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) # Where this ``failure_class`` is explained. ``""`` from older servers. - docs_url: str = "" + docs_url: str = Field(default="", json_schema_extra={"deprecated": True}) @model_validator(mode="before") @classmethod @@ -1042,7 +1042,7 @@ class BatchItemResult(_Lax): task_id: str = "" #: **Deprecated**, use :attr:`claim`. - claim_text: str = "" + claim_text: str = Field(default="", json_schema_extra={"deprecated": True}) status: Literal["completed", "needs_input", "failed", "timeout"] verification: Verification | None = None status_detail: TaskStatus | None = None @@ -1205,7 +1205,7 @@ class Usage(_Lax): #: Empty on servers predating this field — fall back to :attr:`plan`. plan_label: str = "" #: **Deprecated**, use :attr:`UsageCredits.resets_at` (``credits.resets_at``). - quota_resets_at: str | None = None + quota_resets_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) #: The credit balance — the authoritative number. Empty on older servers. credits: UsageCredits = Field(default_factory=UsageCredits) #: Credits per call, keyed by CAPABILITY, at its default price: @@ -1245,11 +1245,11 @@ class Usage(_Lax): #: :attr:`costs` instead:: #: #: left = u.credits.remaining // u.costs["verify"] - verify: UsageCapacity = Field(default_factory=UsageCapacity) + verify: UsageCapacity = Field(default_factory=UsageCapacity, json_schema_extra={"deprecated": True}) #: DEPRECATED, kept for existing code. See :attr:`verify`. - ask: UsageCapacity = Field(default_factory=UsageCapacity) + ask: UsageCapacity = Field(default_factory=UsageCapacity, json_schema_extra={"deprecated": True}) #: DEPRECATED, kept for existing code. See :attr:`verify`. - assess: UsageCapacity = Field(default_factory=UsageCapacity) + assess: UsageCapacity = Field(default_factory=UsageCapacity, json_schema_extra={"deprecated": True}) extract: UsageExtract = Field(default_factory=UsageExtract) # Whether this key has a webhook signing secret provisioned. ``POST /verify`` # with a ``webhook_url`` is rejected without one, so callers that rely on @@ -1373,7 +1373,7 @@ class is explained. """ #: **Deprecated**, use :attr:`code`. - failure_reason: str | None = "" + failure_reason: str | None = Field(default="", json_schema_extra={"deprecated": True}) failure_class: str | None = "" retryable: bool | None = None hint: str | None = None @@ -1491,7 +1491,7 @@ class ReviewSummary(_Lax): claim_limit: int = 20 #: **Deprecated**, use :attr:`claim_limit_exceeded`. The draft held at #: least ``claim_limit`` claims: more MAY exist. - claim_limit_reached: bool | None = None + claim_limit_reached: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) #: The text was cut at 50,000 characters. input_truncated: bool = False assessments: ReviewAssessmentCounts | None = None @@ -1505,7 +1505,7 @@ class ReviewSummary(_Lax): citation_limit: int | None = None #: **Deprecated**, use :attr:`citation_limit_exceeded`. #: ``citations_found`` is over ``citation_limit``. - citation_limit_reached: bool | None = None + citation_limit_reached: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) citation_checks: ReviewCitationCheckCounts | None = None #: ``len(citation_issues)``. citation_issues: int = 0 @@ -1579,11 +1579,11 @@ class ReviewAssessment(_Lax): verification_url: str | None = None #: **Deprecated**, use ``failure.code`` (``no_checkable_claim`` where this #: reads ``no_claim``). - error_code: str | None = None + error_code: str | None = Field(default=None, json_schema_extra={"deprecated": True}) #: **Deprecated**, use :attr:`more_claims`. - identified_claims: list[str] = Field(default_factory=list) + identified_claims: list[str] = Field(default_factory=list, json_schema_extra={"deprecated": True}) #: **Deprecated**, use ``failure.hint`` (on a failed quick check). - hint: str | None = None + hint: str | None = Field(default=None, json_schema_extra={"deprecated": True}) #: With ``suggest_edits=True``: the claim with its wrong part corrected, #: from the quick check's reasoning, when it found the claim "False" or #: "Mostly False" with high confidence. ``None`` otherwise, and from @@ -1656,7 +1656,7 @@ class ReviewVerification(_Lax): created_at: str | None = None #: **Deprecated**, use :attr:`completed_at`. Set only when the check #: completed on a later UTC day than ``created_at``. - modified_at: str | None = None + modified_at: str | None = Field(default=None, json_schema_extra={"deprecated": True}) verification_url: str | None = None url: str | None = None failure: FailureBlock | None = None @@ -1988,7 +1988,7 @@ class CitecheckSummary(_Lax): citations_selected: int | None = None citation_limit: int | None = None #: **Deprecated**, use :attr:`citation_limit_exceeded`. - citation_limit_reached: bool | None = None + citation_limit_reached: bool | None = Field(default=None, json_schema_extra={"deprecated": True}) citation_checks: ReviewCitationCheckCounts | None = None citation_issues: int = 0 diff --git a/tests/test_deprecation_markers.py b/tests/test_deprecation_markers.py new file mode 100644 index 0000000..3ae8122 --- /dev/null +++ b/tests/test_deprecation_markers.py @@ -0,0 +1,56 @@ +"""Every field whose 2.x name is deprecated says so in the JSON schema.""" + +from __future__ import annotations + +import pytest + +from lenz_io import models + +DEPRECATED_FIELDS = { + "CandidateClaim": ["text"], + "ExtractedClaims": ["claim", "identified_claims", "locations", "candidate_claims"], + "AssessClaim": ["error_code", "hint", "identified_claims", "candidate_claims"], + "ReviewAssessment": ["error_code", "hint", "identified_claims"], + "AssessResponse": ["error", "error_code", "candidate_claims"], + "TaskAccepted": ["claim_text"], + "BatchItemResult": ["claim_text"], + "TaskStatus": [ + "error", + "failure_reason", + "failure_detail", + "failure_class", + "retryable", + "docs_url", + "candidates", + "similar_claims", + ], + "FailureBlock": ["failure_reason"], + "Verification": ["modified_at"], + "VerificationListItem": ["modified_at"], + "ReviewVerification": ["modified_at"], + "ReviewSummary": ["claim_limit_reached", "citation_limit_reached"], + "CitecheckSummary": ["citation_limit_reached"], + "Usage": ["quota_resets_at", "verify", "ask", "assess"], + "UsageCredits": ["bonus"], + "UsageCapacity": ["credits"], +} + +#: Fields that stay current: a marker here would warn about a name that has no replacement. +CURRENT_FIELDS = {"TaskStatus": ["hint", "result", "claims"], "Usage": ["credits", "extract"]} + + +def _properties(cls_name: str, mode: str) -> dict[str, dict[str, object]]: + return getattr(models, cls_name).model_json_schema(mode=mode)["properties"] + + +@pytest.mark.parametrize("mode", ["validation", "serialization"]) +@pytest.mark.parametrize(("cls_name", "fields"), sorted(DEPRECATED_FIELDS.items())) +def test_deprecated_fields_are_marked(cls_name: str, fields: list[str], mode: str) -> None: + props = _properties(cls_name, mode) + assert [f for f in fields if props[f].get("deprecated") is not True] == [] + + +@pytest.mark.parametrize(("cls_name", "fields"), sorted(CURRENT_FIELDS.items())) +def test_current_fields_are_not_marked(cls_name: str, fields: list[str]) -> None: + props = _properties(cls_name, "validation") + assert [f for f in fields if props[f].get("deprecated")] == [] diff --git a/tests/test_parity.py b/tests/test_parity.py index 7e0204c..4f603ed 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -226,9 +226,10 @@ def test_model_schemas_match_the_previous_release() -> None: from parity_static import schemas def _shape(schema: Any) -> Any: - # Doc text (class docstrings) may change; the shape may not. + # Doc text (class docstrings) and deprecation markers may change; the + # shape may not. if isinstance(schema, dict): - return {k: _shape(v) for k, v in schema.items() if k != "description"} + return {k: _shape(v) for k, v in schema.items() if k not in ("description", "deprecated")} if isinstance(schema, list): return [_shape(v) for v in schema] return schema From daf2fd3e5c494b31342c383fc131bf970ee038fe Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 12:58:24 +0300 Subject: [PATCH 17/67] test: sync the parity fixtures with the server's recorded cases Rename the stored-replay, stored-progress and live-token cases to the names the server records them under, add the cases that were missing in both shapes (a needs-input 409 and the poll it names, a purged 410, an idempotent ask replay, a retried webhook delivery) with the 2.21 oracle for each, and drop the current-shape twins of the stored replays: the API answers a replay stored by an older release in the old shape and says so in the version header, which the version-guard tests now cover. Co-Authored-By: Claude Opus 5.5 --- .../account__ask_send_idempotent_replay.json} | 8 +- ... => account__oauth_revoke_live_token.json} | 0 .../canonical/extract__replay_later_200.json | 34 ----- ...verify__status_needs_input_hint_names.json | 28 +++++ ...=> verify__stored_progress_completed.json} | 0 ...rify__stored_progress_failed_crashed.json} | 0 ...rogress_failed_insufficient_evidence.json} | 0 ... verify__stored_progress_in_progress.json} | 0 ...erification_not_ready_409_needs_input.json | 15 +++ .../verify__verification_purged_410.json | 13 ++ .../webhook__delivery_retry_restamps.json | 24 ++++ ... account__ask_send_idempotent_replay.json} | 0 .../account__oauth_revoke_live_token.json | 1 + ...00.json => assess__stored_replay_200.json} | 0 ...0.json => extract__stored_replay_200.json} | 0 ...02.json => review__stored_replay_202.json} | 0 ...verify__status_needs_input_hint_names.json | 119 ++++++++++++++++++ ...=> verify__stored_progress_completed.json} | 0 ...rify__stored_progress_failed_crashed.json} | 0 ...rogress_failed_insufficient_evidence.json} | 0 ... verify__stored_progress_in_progress.json} | 0 ...02.json => verify__stored_replay_202.json} | 0 ...erification_not_ready_409_needs_input.json | 24 ++++ .../verify__verification_purged_410.json | 22 ++++ .../webhook__delivery_retry_restamps.json | 15 +++ .../account__ask_send_idempotent_replay.json | 13 ++ ... => account__oauth_revoke_live_token.json} | 0 .../legacy/assess__replay_later_200.json | 28 ----- .../assess__stored_replay_200.json} | 0 ...0.json => extract__stored_replay_200.json} | 0 .../legacy/review__replay_later_202.json | 14 --- .../review__stored_replay_202.json} | 0 ...verify__status_needs_input_hint_names.json | 28 +++++ ...=> verify__stored_progress_completed.json} | 0 ...rify__stored_progress_failed_crashed.json} | 0 ...rogress_failed_insufficient_evidence.json} | 0 ... verify__stored_progress_in_progress.json} | 0 .../verify__stored_replay_202.json} | 0 ...erification_not_ready_409_needs_input.json | 15 +++ .../verify__verification_purged_410.json | 13 ++ .../webhook__delivery_retry_restamps.json | 19 +++ tests/fixtures/parity/pickles.json | 16 +-- tests/parity_observe.py | 8 +- tests/test_api_version_guard.py | 18 ++- 44 files changed, 381 insertions(+), 94 deletions(-) rename tests/fixtures/parity/{legacy/verify__replay_later_202.json => canonical/account__ask_send_idempotent_replay.json} (51%) rename tests/fixtures/parity/canonical/{account__oauth_revoke_token.json => account__oauth_revoke_live_token.json} (100%) delete mode 100644 tests/fixtures/parity/canonical/extract__replay_later_200.json create mode 100644 tests/fixtures/parity/canonical/verify__status_needs_input_hint_names.json rename tests/fixtures/parity/canonical/{verify__status_older_run_completed.json => verify__stored_progress_completed.json} (100%) rename tests/fixtures/parity/canonical/{verify__status_older_run_failed_crashed.json => verify__stored_progress_failed_crashed.json} (100%) rename tests/fixtures/parity/canonical/{verify__status_older_run_failed_insufficient_evidence.json => verify__stored_progress_failed_insufficient_evidence.json} (100%) rename tests/fixtures/parity/canonical/{verify__status_older_run_in_progress.json => verify__stored_progress_in_progress.json} (100%) create mode 100644 tests/fixtures/parity/canonical/verify__verification_not_ready_409_needs_input.json create mode 100644 tests/fixtures/parity/canonical/verify__verification_purged_410.json create mode 100644 tests/fixtures/parity/canonical/webhook__delivery_retry_restamps.json rename tests/fixtures/parity/expected/{account__oauth_revoke_token.json => account__ask_send_idempotent_replay.json} (100%) create mode 100644 tests/fixtures/parity/expected/account__oauth_revoke_live_token.json rename tests/fixtures/parity/expected/{assess__replay_later_200.json => assess__stored_replay_200.json} (100%) rename tests/fixtures/parity/expected/{extract__replay_later_200.json => extract__stored_replay_200.json} (100%) rename tests/fixtures/parity/expected/{review__replay_later_202.json => review__stored_replay_202.json} (100%) create mode 100644 tests/fixtures/parity/expected/verify__status_needs_input_hint_names.json rename tests/fixtures/parity/expected/{verify__status_older_run_completed.json => verify__stored_progress_completed.json} (100%) rename tests/fixtures/parity/expected/{verify__status_older_run_failed_crashed.json => verify__stored_progress_failed_crashed.json} (100%) rename tests/fixtures/parity/expected/{verify__status_older_run_failed_insufficient_evidence.json => verify__stored_progress_failed_insufficient_evidence.json} (100%) rename tests/fixtures/parity/expected/{verify__status_older_run_in_progress.json => verify__stored_progress_in_progress.json} (100%) rename tests/fixtures/parity/expected/{verify__replay_later_202.json => verify__stored_replay_202.json} (100%) create mode 100644 tests/fixtures/parity/expected/verify__verification_not_ready_409_needs_input.json create mode 100644 tests/fixtures/parity/expected/verify__verification_purged_410.json create mode 100644 tests/fixtures/parity/expected/webhook__delivery_retry_restamps.json create mode 100644 tests/fixtures/parity/legacy/account__ask_send_idempotent_replay.json rename tests/fixtures/parity/legacy/{account__oauth_revoke_token.json => account__oauth_revoke_live_token.json} (100%) delete mode 100644 tests/fixtures/parity/legacy/assess__replay_later_200.json rename tests/fixtures/parity/{canonical/assess__replay_later_200.json => legacy/assess__stored_replay_200.json} (100%) rename tests/fixtures/parity/legacy/{extract__replay_later_200.json => extract__stored_replay_200.json} (100%) delete mode 100644 tests/fixtures/parity/legacy/review__replay_later_202.json rename tests/fixtures/parity/{canonical/review__replay_later_202.json => legacy/review__stored_replay_202.json} (100%) create mode 100644 tests/fixtures/parity/legacy/verify__status_needs_input_hint_names.json rename tests/fixtures/parity/legacy/{verify__status_older_run_completed.json => verify__stored_progress_completed.json} (100%) rename tests/fixtures/parity/legacy/{verify__status_older_run_failed_crashed.json => verify__stored_progress_failed_crashed.json} (100%) rename tests/fixtures/parity/legacy/{verify__status_older_run_failed_insufficient_evidence.json => verify__stored_progress_failed_insufficient_evidence.json} (100%) rename tests/fixtures/parity/legacy/{verify__status_older_run_in_progress.json => verify__stored_progress_in_progress.json} (100%) rename tests/fixtures/parity/{canonical/verify__replay_later_202.json => legacy/verify__stored_replay_202.json} (100%) create mode 100644 tests/fixtures/parity/legacy/verify__verification_not_ready_409_needs_input.json create mode 100644 tests/fixtures/parity/legacy/verify__verification_purged_410.json create mode 100644 tests/fixtures/parity/legacy/webhook__delivery_retry_restamps.json diff --git a/tests/fixtures/parity/legacy/verify__replay_later_202.json b/tests/fixtures/parity/canonical/account__ask_send_idempotent_replay.json similarity index 51% rename from tests/fixtures/parity/legacy/verify__replay_later_202.json rename to tests/fixtures/parity/canonical/account__ask_send_idempotent_replay.json index 1991e8a..b387511 100644 --- a/tests/fixtures/parity/legacy/verify__replay_later_202.json +++ b/tests/fixtures/parity/canonical/account__ask_send_idempotent_replay.json @@ -1,9 +1,9 @@ { - "status": 202, + "status": 200, "body": { - "task_id": "7cff18da2d979d6370e54a160be121ad", - "status": "queued", - "chain_id": "99a0a9ca89e1e72c" + "role": "expert", + "content": "Because.", + "created_at": "2026-10-01T09:07:00.123456+00:00" }, "headers": { "Content-Type": "application/json", diff --git a/tests/fixtures/parity/canonical/account__oauth_revoke_token.json b/tests/fixtures/parity/canonical/account__oauth_revoke_live_token.json similarity index 100% rename from tests/fixtures/parity/canonical/account__oauth_revoke_token.json rename to tests/fixtures/parity/canonical/account__oauth_revoke_live_token.json diff --git a/tests/fixtures/parity/canonical/extract__replay_later_200.json b/tests/fixtures/parity/canonical/extract__replay_later_200.json deleted file mode 100644 index d1b6b07..0000000 --- a/tests/fixtures/parity/canonical/extract__replay_later_200.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "status": 200, - "body": { - "status": "ready", - "claims": [ - { - "claim": "Alpha rose 5% in 2024.", - "positions": null - }, - { - "claim": "Beta fell 3% last year.", - "positions": null - } - ], - "domain": "Economics", - "key_entities": [ - { - "name": "Alpha", - "type": "organization" - }, - { - "name": "Beta", - "type": "organization" - } - ], - "presumed_intent": "Verify reported figures", - "original_input": "Alpha rose 5% in 2024. Beta fell 3% last year." - }, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - } -} diff --git a/tests/fixtures/parity/canonical/verify__status_needs_input_hint_names.json b/tests/fixtures/parity/canonical/verify__status_needs_input_hint_names.json new file mode 100644 index 0000000..ee572f4 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_needs_input_hint_names.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "body": { + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "reason": "multi_claim", + "claims": [ + { + "claim": "A.", + "domain": "" + }, + { + "claim": "A happened.", + "domain": "" + }, + { + "claim": "B happened.", + "domain": "" + } + ], + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_completed.json b/tests/fixtures/parity/canonical/verify__stored_progress_completed.json similarity index 100% rename from tests/fixtures/parity/canonical/verify__status_older_run_completed.json rename to tests/fixtures/parity/canonical/verify__stored_progress_completed.json diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json b/tests/fixtures/parity/canonical/verify__stored_progress_failed_crashed.json similarity index 100% rename from tests/fixtures/parity/canonical/verify__status_older_run_failed_crashed.json rename to tests/fixtures/parity/canonical/verify__stored_progress_failed_crashed.json diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json b/tests/fixtures/parity/canonical/verify__stored_progress_failed_insufficient_evidence.json similarity index 100% rename from tests/fixtures/parity/canonical/verify__status_older_run_failed_insufficient_evidence.json rename to tests/fixtures/parity/canonical/verify__stored_progress_failed_insufficient_evidence.json diff --git a/tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json b/tests/fixtures/parity/canonical/verify__stored_progress_in_progress.json similarity index 100% rename from tests/fixtures/parity/canonical/verify__status_older_run_in_progress.json rename to tests/fixtures/parity/canonical/verify__stored_progress_in_progress.json diff --git a/tests/fixtures/parity/canonical/verify__verification_not_ready_409_needs_input.json b/tests/fixtures/parity/canonical/verify__verification_not_ready_409_needs_input.json new file mode 100644 index 0000000..efbdf4f --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_not_ready_409_needs_input.json @@ -0,0 +1,15 @@ +{ + "status": 409, + "body": { + "detail": "This check is waiting for your input.", + "code": "verification_not_ready", + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "hint": "GET /verify/status/7cff18da2d979d6370e54a160be121ad lists the options and how to resolve them." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/verify__verification_purged_410.json b/tests/fixtures/parity/canonical/verify__verification_purged_410.json new file mode 100644 index 0000000..924d468 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__verification_purged_410.json @@ -0,0 +1,13 @@ +{ + "status": 410, + "body": { + "detail": "This verification is no longer available: its account removes verifications after a set period.", + "code": "purged", + "purged_at": "2026-10-01T09:07:00.123456+00:00" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__delivery_retry_restamps.json b/tests/fixtures/parity/canonical/webhook__delivery_retry_restamps.json new file mode 100644 index 0000000..14a10f3 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__delivery_retry_restamps.json @@ -0,0 +1,24 @@ +{ + "status": 200, + "body": { + "attempt": 2, + "delivered_at": "2026-10-01T09:07:00.123456+00:00", + "event": "verification.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification": { + "failure": { + "code": "conclusion_failed", + "detail": "The check stopped while writing the conclusion.", + "docs_url": "https://lenz.io/docs/errors#internal", + "failure_class": "internal", + "hint": null, + "retryable": false + }, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + } + }, + "headers": {} +} diff --git a/tests/fixtures/parity/expected/account__oauth_revoke_token.json b/tests/fixtures/parity/expected/account__ask_send_idempotent_replay.json similarity index 100% rename from tests/fixtures/parity/expected/account__oauth_revoke_token.json rename to tests/fixtures/parity/expected/account__ask_send_idempotent_replay.json diff --git a/tests/fixtures/parity/expected/account__oauth_revoke_live_token.json b/tests/fixtures/parity/expected/account__oauth_revoke_live_token.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/parity/expected/account__oauth_revoke_live_token.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/parity/expected/assess__replay_later_200.json b/tests/fixtures/parity/expected/assess__stored_replay_200.json similarity index 100% rename from tests/fixtures/parity/expected/assess__replay_later_200.json rename to tests/fixtures/parity/expected/assess__stored_replay_200.json diff --git a/tests/fixtures/parity/expected/extract__replay_later_200.json b/tests/fixtures/parity/expected/extract__stored_replay_200.json similarity index 100% rename from tests/fixtures/parity/expected/extract__replay_later_200.json rename to tests/fixtures/parity/expected/extract__stored_replay_200.json diff --git a/tests/fixtures/parity/expected/review__replay_later_202.json b/tests/fixtures/parity/expected/review__stored_replay_202.json similarity index 100% rename from tests/fixtures/parity/expected/review__replay_later_202.json rename to tests/fixtures/parity/expected/review__stored_replay_202.json diff --git a/tests/fixtures/parity/expected/verify__status_needs_input_hint_names.json b/tests/fixtures/parity/expected/verify__status_needs_input_hint_names.json new file mode 100644 index 0000000..713def0 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_needs_input_hint_names.json @@ -0,0 +1,119 @@ +{ + "dump": { + "candidates": [], + "claims": [ + { + "domain": "", + "text": "A." + }, + { + "domain": "", + "text": "A happened." + }, + { + "domain": "", + "text": "B happened." + } + ], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "multi_claim", + "result": null, + "retryable": null, + "similar_claims": [], + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "dump_unset": { + "claims": [ + { + "domain": "", + "text": "A." + }, + { + "domain": "", + "text": "A happened." + }, + { + "domain": "", + "text": "B happened." + } + ], + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "reason": "multi_claim", + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "needs input (multi_claim)\nclaims found:\n 1. A.\n 2. A happened.\n 3. B happened.\nresolve it: lenz verify --resume t --claim --detach\n", + "render_json": "{\n \"status\": \"needs_input\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"multi_claim\",\n \"hint\": \"The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [\n {\n \"text\": \"A.\",\n \"domain\": \"\"\n },\n {\n \"text\": \"A happened.\",\n \"domain\": \"\"\n },\n {\n \"text\": \"B happened.\",\n \"domain\": \"\"\n }\n ],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"\",\n \"failure_reason\": \"\",\n \"failure_detail\": \"\",\n \"failure_class\": \"\",\n \"retryable\": null,\n \"docs_url\": \"\"\n}\n", + "repr": "TaskStatus(status='needs_input', task_id='7cff18da2d979d6370e54a160be121ad', reason='multi_claim', hint='The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[CandidateClaim(text='A.', domain=''), CandidateClaim(text='A happened.', domain=''), CandidateClaim(text='B happened.', domain='')], candidates=[], similar_claims=[], error='', failure_reason='', failure_detail='', failure_class='', retryable=None, docs_url='')", + "wait": { + "cause": "The verification needs caller input to proceed.", + "code": "", + "doc_url": "https://lenz.io/docs/verify#needs-input", + "fix": "Inspect the payload, then call client.select(task_id, texts=[...]) with the chosen claim(s).", + "friendly_text": "Pipeline paused: multi_claim\n Fix: Inspect the payload, then call client.select(task_id, texts=[...]) with the chosen claim(s).", + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "kind": "multi_claim", + "message": "Pipeline paused: multi_claim", + "payload": { + "candidates": [], + "claims": [ + { + "domain": "", + "text": "A." + }, + { + "domain": "", + "text": "A happened." + }, + { + "domain": "", + "text": "B happened." + } + ], + "docs_url": "", + "error": "", + "failure_class": "", + "failure_detail": "", + "failure_reason": "", + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select.", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "multi_claim", + "result": null, + "retryable": null, + "similar_claims": [], + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "payload_json": { + "error": { + "code": "needs_input", + "fix": "Inspect the payload, then call client.select(task_id, texts=[...]) with the chosen claim(s).", + "message": "Pipeline paused: multi_claim", + "status": 0 + } + }, + "request_id": "", + "status_code": 0, + "task_id": "t", + "type": "LenzNeedsInputError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_older_run_completed.json b/tests/fixtures/parity/expected/verify__stored_progress_completed.json similarity index 100% rename from tests/fixtures/parity/expected/verify__status_older_run_completed.json rename to tests/fixtures/parity/expected/verify__stored_progress_completed.json diff --git a/tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json b/tests/fixtures/parity/expected/verify__stored_progress_failed_crashed.json similarity index 100% rename from tests/fixtures/parity/expected/verify__status_older_run_failed_crashed.json rename to tests/fixtures/parity/expected/verify__stored_progress_failed_crashed.json diff --git a/tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json b/tests/fixtures/parity/expected/verify__stored_progress_failed_insufficient_evidence.json similarity index 100% rename from tests/fixtures/parity/expected/verify__status_older_run_failed_insufficient_evidence.json rename to tests/fixtures/parity/expected/verify__stored_progress_failed_insufficient_evidence.json diff --git a/tests/fixtures/parity/expected/verify__status_older_run_in_progress.json b/tests/fixtures/parity/expected/verify__stored_progress_in_progress.json similarity index 100% rename from tests/fixtures/parity/expected/verify__status_older_run_in_progress.json rename to tests/fixtures/parity/expected/verify__stored_progress_in_progress.json diff --git a/tests/fixtures/parity/expected/verify__replay_later_202.json b/tests/fixtures/parity/expected/verify__stored_replay_202.json similarity index 100% rename from tests/fixtures/parity/expected/verify__replay_later_202.json rename to tests/fixtures/parity/expected/verify__stored_replay_202.json diff --git a/tests/fixtures/parity/expected/verify__verification_not_ready_409_needs_input.json b/tests/fixtures/parity/expected/verify__verification_not_ready_409_needs_input.json new file mode 100644 index 0000000..ae73788 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_not_ready_409_needs_input.json @@ -0,0 +1,24 @@ +{ + "error": { + "cause": "This check is waiting for your input.", + "code": "verification_not_ready", + "doc_url": "https://lenz.io/docs/verify", + "fix": "GET /verify/status/7cff18da2d979d6370e54a160be121ad lists the options and how to resolve them.", + "friendly_text": "This check is waiting for your input.\n Fix: GET /verify/status/7cff18da2d979d6370e54a160be121ad lists the options and how to resolve them.", + "hint": "GET /verify/status/7cff18da2d979d6370e54a160be121ad lists the options and how to resolve them.", + "message": "This check is waiting for your input.", + "payload_json": { + "error": { + "code": "not_ready", + "fix": "GET /verify/status/7cff18da2d979d6370e54a160be121ad lists the options and how to resolve them.", + "message": "This check is waiting for your input.", + "status": 409 + } + }, + "request_id": "", + "status": "needs_input", + "status_code": 409, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "LenzVerificationNotReadyError" + } +} diff --git a/tests/fixtures/parity/expected/verify__verification_purged_410.json b/tests/fixtures/parity/expected/verify__verification_purged_410.json new file mode 100644 index 0000000..99f659b --- /dev/null +++ b/tests/fixtures/parity/expected/verify__verification_purged_410.json @@ -0,0 +1,22 @@ +{ + "error": { + "cause": "This verification is no longer available: its account removes verifications after a set period.", + "code": "purged", + "doc_url": "https://lenz.io/docs/errors", + "fix": "Its account's retention period removed it. A certificate issued for it is still available.", + "friendly_text": "This verification is no longer available: its account removes verifications after a set period.\n Fix: Its account's retention period removed it. A certificate issued for it is still available.", + "message": "This verification is no longer available: its account removes verifications after a set period.", + "payload_json": { + "error": { + "code": "gone", + "fix": "Its account's retention period removed it. A certificate issued for it is still available.", + "message": "This verification is no longer available: its account removes verifications after a set period.", + "status": 410 + } + }, + "purged_at": "2026-10-01T09:07:00.123456+00:00", + "request_id": "", + "status_code": 410, + "type": "LenzGoneError" + } +} diff --git a/tests/fixtures/parity/expected/webhook__delivery_retry_restamps.json b/tests/fixtures/parity/expected/webhook__delivery_retry_restamps.json new file mode 100644 index 0000000..fa818f6 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__delivery_retry_restamps.json @@ -0,0 +1,15 @@ +{ + "event": { + "attempt": 2, + "batch_id": null, + "delivered_at": "2026-10-01T09:07:00.123456+00:00", + "error": "conclusion_failed", + "event": "verification.failed", + "failure_class": "internal", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "VerificationFailed", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/legacy/account__ask_send_idempotent_replay.json b/tests/fixtures/parity/legacy/account__ask_send_idempotent_replay.json new file mode 100644 index 0000000..b387511 --- /dev/null +++ b/tests/fixtures/parity/legacy/account__ask_send_idempotent_replay.json @@ -0,0 +1,13 @@ +{ + "status": 200, + "body": { + "role": "expert", + "content": "Because.", + "created_at": "2026-10-01T09:07:00.123456+00:00" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/account__oauth_revoke_token.json b/tests/fixtures/parity/legacy/account__oauth_revoke_live_token.json similarity index 100% rename from tests/fixtures/parity/legacy/account__oauth_revoke_token.json rename to tests/fixtures/parity/legacy/account__oauth_revoke_live_token.json diff --git a/tests/fixtures/parity/legacy/assess__replay_later_200.json b/tests/fixtures/parity/legacy/assess__replay_later_200.json deleted file mode 100644 index fedc9c5..0000000 --- a/tests/fixtures/parity/legacy/assess__replay_later_200.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "status": 200, - "body": { - "claims": [ - { - "claim": "The Earth orbits the Sun.", - "language": "en", - "verdict": "True", - "confidence": "high", - "verification_url": null, - "rationale": null, - "dissent": null, - "suggested_rewrite": null, - "error_code": null, - "candidate_claims": [], - "identified_claims": [], - "hint": null - } - ], - "error": null, - "more_claims": [] - }, - "headers": { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Vary": "origin" - } -} diff --git a/tests/fixtures/parity/canonical/assess__replay_later_200.json b/tests/fixtures/parity/legacy/assess__stored_replay_200.json similarity index 100% rename from tests/fixtures/parity/canonical/assess__replay_later_200.json rename to tests/fixtures/parity/legacy/assess__stored_replay_200.json diff --git a/tests/fixtures/parity/legacy/extract__replay_later_200.json b/tests/fixtures/parity/legacy/extract__stored_replay_200.json similarity index 100% rename from tests/fixtures/parity/legacy/extract__replay_later_200.json rename to tests/fixtures/parity/legacy/extract__stored_replay_200.json diff --git a/tests/fixtures/parity/legacy/review__replay_later_202.json b/tests/fixtures/parity/legacy/review__replay_later_202.json deleted file mode 100644 index ea99cf8..0000000 --- a/tests/fixtures/parity/legacy/review__replay_later_202.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "status": 202, - "body": { - "review_id": "d6b2bd72", - "status": "queued" - }, - "headers": { - "Content-Type": "application/json", - "Retry-After": "20", - "Location": "/api/v1/reviews/d6b2bd72", - "Cache-Control": "no-store", - "Vary": "origin" - } -} diff --git a/tests/fixtures/parity/canonical/review__replay_later_202.json b/tests/fixtures/parity/legacy/review__stored_replay_202.json similarity index 100% rename from tests/fixtures/parity/canonical/review__replay_later_202.json rename to tests/fixtures/parity/legacy/review__stored_replay_202.json diff --git a/tests/fixtures/parity/legacy/verify__status_needs_input_hint_names.json b/tests/fixtures/parity/legacy/verify__status_needs_input_hint_names.json new file mode 100644 index 0000000..09d3e99 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_needs_input_hint_names.json @@ -0,0 +1,28 @@ +{ + "status": 200, + "body": { + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "reason": "multi_claim", + "claims": [ + { + "text": "A.", + "domain": "" + }, + { + "text": "A happened.", + "domain": "" + }, + { + "text": "B happened.", + "domain": "" + } + ], + "hint": "The text holds several distinct claims. Send the ones to check to POST /verify/{task_id}/select." + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_completed.json b/tests/fixtures/parity/legacy/verify__stored_progress_completed.json similarity index 100% rename from tests/fixtures/parity/legacy/verify__status_older_run_completed.json rename to tests/fixtures/parity/legacy/verify__stored_progress_completed.json diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json b/tests/fixtures/parity/legacy/verify__stored_progress_failed_crashed.json similarity index 100% rename from tests/fixtures/parity/legacy/verify__status_older_run_failed_crashed.json rename to tests/fixtures/parity/legacy/verify__stored_progress_failed_crashed.json diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json b/tests/fixtures/parity/legacy/verify__stored_progress_failed_insufficient_evidence.json similarity index 100% rename from tests/fixtures/parity/legacy/verify__status_older_run_failed_insufficient_evidence.json rename to tests/fixtures/parity/legacy/verify__stored_progress_failed_insufficient_evidence.json diff --git a/tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json b/tests/fixtures/parity/legacy/verify__stored_progress_in_progress.json similarity index 100% rename from tests/fixtures/parity/legacy/verify__status_older_run_in_progress.json rename to tests/fixtures/parity/legacy/verify__stored_progress_in_progress.json diff --git a/tests/fixtures/parity/canonical/verify__replay_later_202.json b/tests/fixtures/parity/legacy/verify__stored_replay_202.json similarity index 100% rename from tests/fixtures/parity/canonical/verify__replay_later_202.json rename to tests/fixtures/parity/legacy/verify__stored_replay_202.json diff --git a/tests/fixtures/parity/legacy/verify__verification_not_ready_409_needs_input.json b/tests/fixtures/parity/legacy/verify__verification_not_ready_409_needs_input.json new file mode 100644 index 0000000..efbdf4f --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_not_ready_409_needs_input.json @@ -0,0 +1,15 @@ +{ + "status": 409, + "body": { + "detail": "This check is waiting for your input.", + "code": "verification_not_ready", + "status": "needs_input", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "hint": "GET /verify/status/7cff18da2d979d6370e54a160be121ad lists the options and how to resolve them." + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/verify__verification_purged_410.json b/tests/fixtures/parity/legacy/verify__verification_purged_410.json new file mode 100644 index 0000000..924d468 --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__verification_purged_410.json @@ -0,0 +1,13 @@ +{ + "status": 410, + "body": { + "detail": "This verification is no longer available: its account removes verifications after a set period.", + "code": "purged", + "purged_at": "2026-10-01T09:07:00.123456+00:00" + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__delivery_retry_restamps.json b/tests/fixtures/parity/legacy/webhook__delivery_retry_restamps.json new file mode 100644 index 0000000..36f3541 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__delivery_retry_restamps.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "body": { + "attempt": 2, + "batch_id": null, + "coverage": null, + "delivered_at": "2026-10-01T09:07:00.123456+00:00", + "error": "conclusion_failed", + "event": "verification.failed", + "failure_class": "internal", + "needs_input": null, + "result": null, + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "verification_id": null + }, + "headers": {} +} diff --git a/tests/fixtures/parity/pickles.json b/tests/fixtures/parity/pickles.json index cf1551e..d096ce5 100644 --- a/tests/fixtures/parity/pickles.json +++ b/tests/fixtures/parity/pickles.json @@ -23,7 +23,7 @@ "assess__list_compound_item.json": "gASV2wIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMDlByaW1hcnkgY2xhaW0ulIwIbGFuZ3VhZ2WUjAJlbpSMB3ZlcmRpY3SUjAxNb3N0bHkgRmFsc2WUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTowKZXJyb3JfY29kZZROjBBjYW5kaWRhdGVfY2xhaW1zlF2UjBFpZGVudGlmaWVkX2NsYWltc5RdlCiMDVNlY29uZCBjbGFpbS6UjAxUaGlyZCBjbGFpbS6UZYwEaGludJSMWkFzc2Vzc2VkIHRoZSBtYWluIGNsYWltIG9ubHkuIFNlbmQgaWRlbnRpZmllZF9jbGFpbXMgYXMgdGhlaXIgb3duIGl0ZW1zIHRvIGNoZWNrIHRoZSByZXN0LpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCFoGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgKKYGUfZQoaAV9lChoDowOQSBwbGFpbiBjbGFpbS6UaBCMAmVulGgSjAxNb3N0bHkgRmFsc2WUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoIU51aCN9lGglj5QoaA5oIWgYaBtoFmgQaBRoHWgSaBdoGmgZkGgnTnViZYwFZXJyb3KUTmgajACUaBtdlIwLbW9yZV9jbGFpbXOUXZR1aCN9lGglj5QoaAdoNmgzkGgnTnViLg==", "assess__list_mixed_rows.json": "gASVxAQAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMIldhdGVyIGJvaWxzIGF0IDEwMCBDIGF0IHNlYSBsZXZlbC6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTowKZXJyb3JfY29kZZROjBBjYW5kaWRhdGVfY2xhaW1zlF2UjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoCimBlH2UKGgFfZQoaA6MC2hlbGxvIHRoZXJllGgQjAJlbpRoEowFRXJyb3KUaBSMA2xvd5RoFk5oF05oGE5oGU5oGowIbm9fY2xhaW2UaBtdlGgdXZRoH4xqVGhlIGlucHV0IGlzIGEgZ3JlZXRpbmcuIFNlbmQgb25lIGZhY3R1YWwgY2xhaW0sIG9yIHJ1biB0aGUgdGV4dCB0aHJvdWdoIC9leHRyYWN0IHRvIGVudW1lcmF0ZSBpdHMgY2xhaW1zLpR1aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjA52ZW5kb3IgaXMgZG93bpRoEIwCZW6UaBKMBUVycm9ylGgUjANsb3eUaBZOaBdOaBhOaBlOaBqMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGgbXZRoHV2UaB+MTkEgbW9kZWwgcHJvdmlkZXIgd2FzIHVuYXZhaWxhYmxlIGZvciB0aGlzIGl0ZW0uIFJldHJ5IGl0OyBub3RoaW5nIHdhcyBjaGFyZ2VkLpR1aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjAxjYW5ub3QgZnJhbWWUaBCMAmVulGgSjAVFcnJvcpRoFIwDbG93lGgWTmgXTmgYTmgZTmgajA5mcmFtaW5nX2ZhaWxlZJRoG12UaB1dlGgfjINUaGlzIGl0ZW0gY291bGQgbm90IGJlIHByb2Nlc3NlZDsgcmV0cnlpbmcgaXQgYXMtaXMgd2lsbCBub3QgaGVscC4gU2VuZCBpdCByZXBocmFzZWQgYXMgb25lIGZhY3R1YWwgc3RhdGVtZW50OyBub3RoaW5nIHdhcyBjaGFyZ2VkLpR1aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViZYwFZXJyb3KUTmgajACUaBtdlIwLbW9yZV9jbGFpbXOUXZR1aCB9lGgij5QoaAdoT2hMkGgkTnViLg==", "assess__memo_hit.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwgVGhlIERhbnViZSBmbG93cyB0aHJvdWdoIFZpZW5uYS6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllIwgVGhlIHJpdmVyIHJ1bnMgdGhyb3VnaCB0aGUgY2l0eS6UjAdkaXNzZW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmVycm9yX2NvZGWUTowQY2FuZGlkYXRlX2NsYWltc5RdlIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA5oIGgZaBxoFmgQaBRoHmgSaBdoG2gakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViYYwFZXJyb3KUTmgbjACUaBxdlIwLbW9yZV9jbGFpbXOUXZR1aCF9lGgjj5QoaAdoKWgmkGglTnViLg==", - "assess__replay_later_200.json": "gASV3QEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwZVGhlIEVhcnRoIG9yYml0cyB0aGUgU3VuLpSMCGxhbmd1YWdllIwCZW6UjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwQdmVyaWZpY2F0aW9uX3VybJROjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UdWggfZRoIo+UKGgHaChoJZBoJE51Yi4=", + "assess__stored_replay_200.json": "gASV3QEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwZVGhlIEVhcnRoIG9yYml0cyB0aGUgU3VuLpSMCGxhbmd1YWdllIwCZW6UjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwQdmVyaWZpY2F0aW9uX3VybJROjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UdWggfZRoIo+UKGgHaChoJZBoJE51Yi4=", "assess__single_no_claim.json": "gASVAgEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UjAVlcnJvcpSMHE5vIHZlcmlmaWFibGUgY2xhaW0gZGV0ZWN0ZWSUjAplcnJvcl9jb2RllIwIbm9fY2xhaW2UjBBjYW5kaWRhdGVfY2xhaW1zlF2UjAttb3JlX2NsYWltc5RdlHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA1oD2gLaAdoCZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", "assess__single_one_claim.json": "gASViwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwsVGhlIHJlZ2lzdHJ5IHJlcG9ydGVkIDQsMjAwIGZpbGluZ3MgaW4gMjAyNC6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllIxJVGhlIHJlZ2lzdHJ5IGxpc3RzIDQsMjAwIGZpbGluZ3MgZm9yIDIwMjQgYW5kIGhhcyBub3QgcmV2aXNlZCB0aGUgZmlndXJlLpSMB2Rpc3NlbnSUjE5UaGUgcmVnaXN0cnkgZmlndXJlIGlzIHByb3Zpc2lvbmFsLCBzbyB0aGUgMjAyNCB0b3RhbCBjYW5ub3QgeWV0IGJlIGNvbmZpcm1lZC6UjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCFoGWgdaBZoEGgUaB9oEmgXaBxoG5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oHIwAlGgdXZSMC21vcmVfY2xhaW1zlF2UdWgifZRoJI+UKGgHaCpoJ5BoJk51Yi4=", "assess__single_text_over_wave_more_claims.json": "gASVbgwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMHUNsYWltIG51bWJlciAwIGlzIGRvY3VtZW50ZWQulIwIbGFuZ3VhZ2WUjAJlbpSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjBB2ZXJpZmljYXRpb25fdXJslE6MCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmVycm9yX2NvZGWUTowQY2FuZGlkYXRlX2NsYWltc5RdlIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgMSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgMiBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgMyBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNCBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNiBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNyBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgOCBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgOSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTAgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDoweQ2xhaW0gbnVtYmVyIDExIGlzIGRvY3VtZW50ZWQulGgQjAJlbpRoEowEVHJ1ZZRoFIwEaGlnaJRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJoCimBlH2UKGgFfZQoaA6MHkNsYWltIG51bWJlciAxMiBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTMgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDoweQ2xhaW0gbnVtYmVyIDE0IGlzIGRvY3VtZW50ZWQulGgQjAJlbpRoEowEVHJ1ZZRoFIwEaGlnaJRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJoCimBlH2UKGgFfZQoaA6MHkNsYWltIG51bWJlciAxNSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTYgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDoweQ2xhaW0gbnVtYmVyIDE3IGlzIGRvY3VtZW50ZWQulGgQjAJlbpRoEowEVHJ1ZZRoFIwEaGlnaJRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJoCimBlH2UKGgFfZQoaA6MHkNsYWltIG51bWJlciAxOCBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTkgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmWMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UKIweQ2xhaW0gbnVtYmVyIDIwIGlzIGRvY3VtZW50ZWQulIweQ2xhaW0gbnVtYmVyIDIxIGlzIGRvY3VtZW50ZWQulGV1aCB9lGgij5QoaAdo+Wj2kGgkTnViLg==", @@ -56,7 +56,7 @@ "extract__not_a_claim_beside_claims.json": "gASVHwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMC25vdF9hX2NsYWltlIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMEGNhbmRpZGF0ZV9jbGFpbXOUXZSMBmRvbWFpbpSMCUVjb25vbWljc5SMDGtleV9lbnRpdGllc5RdlChoAIwPRXh0cmFjdGVkRW50aXR5lJOUKYGUfZQoaAV9lCiMBG5hbWWUjAVBbHBoYZSMBHR5cGWUjAxvcmdhbml6YXRpb26UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaBQpgZR9lChoBX2UKGgYjARCZXRhlGgajAxvcmdhbml6YXRpb26UdWgcfZRoHo+UKGgYaBqQaCBOdWJljA9wcmVzdW1lZF9pbnRlbnSUjBdWZXJpZnkgcmVwb3J0ZWQgZmlndXJlc5SMDm9yaWdpbmFsX2lucHV0lIwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpSMCWxvY2F0aW9uc5ROdWgcfZRoHo+UKGgRaAloDWgLaAdoD2gsaCpoKJBoIE51Yi4=", "extract__ready_one_claim.json": "gASVGQIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMEGNhbmRpZGF0ZV9jbGFpbXOUXZSMBmRvbWFpbpSMCUVjb25vbWljc5SMDGtleV9lbnRpdGllc5RdlChoAIwPRXh0cmFjdGVkRW50aXR5lJOUKYGUfZQoaAV9lCiMBG5hbWWUjAVBbHBoYZSMBHR5cGWUjAxvcmdhbml6YXRpb26UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaBQpgZR9lChoBX2UKGgYjARCZXRhlGgajAxvcmdhbml6YXRpb26UdWgcfZRoHo+UKGgYaBqQaCBOdWJljA9wcmVzdW1lZF9pbnRlbnSUjBdWZXJpZnkgcmVwb3J0ZWQgZmlndXJlc5SMDm9yaWdpbmFsX2lucHV0lIwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpSMCWxvY2F0aW9uc5ROdWgcfZRoHo+UKGgRaAloDWgLaAdoD2gsaCpoKJBoIE51Yi4=", "extract__ready_several_claims.json": "gASVZgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZQojBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwXQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UZYwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulIwJRWNvbm9taWNzlIwMa2V5X2VudGl0aWVzlF2UKGgAjA9FeHRyYWN0ZWRFbnRpdHmUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUFscGhhlIwEdHlwZZSMDG9yZ2FuaXphdGlvbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaByQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoFimBlH2UKGgFfZQoaBqMBEJldGGUaByMDG9yZ2FuaXphdGlvbpR1aB59lGggj5QoaBpoHJBoIk51YmWMD3ByZXN1bWVkX2ludGVudJSMF1ZlcmlmeSByZXBvcnRlZCBmaWd1cmVzlIwOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlE51aB59lGggj5QoaBNoCWgPaAtoB2gRaC5oLGgqkGgiTnViLg==", - "extract__replay_later_200.json": "gASVZgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZQojBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwXQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UZYwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulIwJRWNvbm9taWNzlIwMa2V5X2VudGl0aWVzlF2UKGgAjA9FeHRyYWN0ZWRFbnRpdHmUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUFscGhhlIwEdHlwZZSMDG9yZ2FuaXphdGlvbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaByQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoFimBlH2UKGgFfZQoaBqMBEJldGGUaByMDG9yZ2FuaXphdGlvbpR1aB59lGggj5QoaBpoHJBoIk51YmWMD3ByZXN1bWVkX2ludGVudJSMF1ZlcmlmeSByZXBvcnRlZCBmaWd1cmVzlIwOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlE51aB59lGggj5QoaBNoCWgPaAtoB2gRaC5oLGgqkGgiTnViLg==", + "extract__stored_replay_200.json": "gASVZgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZQojBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwXQmV0YSBmZWxsIDMlIGxhc3QgeWVhci6UZYwQY2FuZGlkYXRlX2NsYWltc5RdlIwGZG9tYWlulIwJRWNvbm9taWNzlIwMa2V5X2VudGl0aWVzlF2UKGgAjA9FeHRyYWN0ZWRFbnRpdHmUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUFscGhhlIwEdHlwZZSMDG9yZ2FuaXphdGlvbpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaByQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoFimBlH2UKGgFfZQoaBqMBEJldGGUaByMDG9yZ2FuaXphdGlvbpR1aB59lGggj5QoaBpoHJBoIk51YmWMD3ByZXN1bWVkX2ludGVudJSMF1ZlcmlmeSByZXBvcnRlZCBmaWd1cmVzlIwOb3JpZ2luYWxfaW5wdXSUjC5BbHBoYSByb3NlIDUlIGluIDIwMjQuIEJldGEgZmVsbCAzJSBsYXN0IHllYXIulIwJbG9jYXRpb25zlE51aB59lGggj5QoaBNoCWgPaAtoB2gRaC5oLGgqkGgiTnViLg==", "extract__url_input_located.json": "gASV4AIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwPRXh0cmFjdGVkQ2xhaW1zlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMBnN0YXR1c5SMBXJlYWR5lIwFY2xhaW2UjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMEGNhbmRpZGF0ZV9jbGFpbXOUXZSMBmRvbWFpbpSMCUVjb25vbWljc5SMDGtleV9lbnRpdGllc5RdlChoAIwPRXh0cmFjdGVkRW50aXR5lJOUKYGUfZQoaAV9lCiMBG5hbWWUjAVBbHBoYZSMBHR5cGWUjAxvcmdhbml6YXRpb26UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaBQpgZR9lChoBX2UKGgYjARCZXRhlGgajAxvcmdhbml6YXRpb26UdWgcfZRoHo+UKGgYaBqQaCBOdWJljA9wcmVzdW1lZF9pbnRlbnSUjBdWZXJpZnkgcmVwb3J0ZWQgZmlndXJlc5SMDm9yaWdpbmFsX2lucHV0lIweaHR0cHM6Ly9leGFtcGxlLmNvbS9hbi1hcnRpY2xllIwJbG9jYXRpb25zlF2UaACMDUNsYWltTG9jYXRpb26Uk5QpgZR9lChoBX2UKGgJjBZBbHBoYSByb3NlIDUlIGluIDIwMjQulIwJcG9zaXRpb25zlF2UaACMCFBvc2l0aW9ulJOUKYGUfZQoaAV9lCiMBXN0YXJ0lE6MA2VuZJROjAR0ZXh0lIwWQWxwaGEgcm9zZSA1JSBpbiAyMDI0LpR1aBx9lGgej5QoaDtoPGg9kGggTnViYXVoHH2UaB6PlChoCWg0kGggTnViYXVoHH2UaB6PlChoEWgJaA1oC2gHaA9oLGgqaCiQaCBOdWIu", "review__get_assessing.json": "gASVTgcAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJYXNzZXNzaW5nlIwHb3V0Y29tZZROjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lIwbMjAyNi0xMC0wMVQxMDowNzowMC4xMjM0NTZalIwIbGFuZ3VhZ2WUjAJlbpSMBnBvbGljeZRoAIwQRXNjYWxhdGlvblBvbGljeZSTlCmBlH2UKGgFfZQojAh2ZXJkaWN0c5RdlCiMBUZhbHNllIwMTW9zdGx5IEZhbHNllIwFTWl4ZWSUZYwKY29uZmlkZW5jZZRdlIwDbG93lGGMD21heF9hc3Nlc3NtZW50c5RLFIwRbWF4X3ZlcmlmaWNhdGlvbnOUSwWMBWRlcHRolIwIc3RhbmRhcmSUjA1tYXhfY2l0YXRpb25zlEsAjA1zdWdnZXN0X2VkaXRzlIl1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgaaCRoH2gmaCdoI2gikIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjA1SZXZpZXdTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NsYWltc19zZWxlY3RlZJRLAowLY2xhaW1fbGltaXSUSxSME2NsYWltX2xpbWl0X3JlYWNoZWSUiYwPaW5wdXRfdHJ1bmNhdGVklImMC2Fzc2Vzc21lbnRzlGgAjBZSZXZpZXdBc3Nlc3NtZW50Q291bnRzlJOUKYGUfZQoaAV9lCiMCWNvbXBsZXRlZJRLAIwGZmFpbGVklEsAdWgofZRoKo+UKGg9aD6QaCxOdWKMDXZlcmlmaWNhdGlvbnOUTowGaXNzdWVzlEsAjA9jaXRhdGlvbnNfZm91bmSUTowSY2l0YXRpb25zX3NlbGVjdGVklE6MDmNpdGF0aW9uX2xpbWl0lE6MFmNpdGF0aW9uX2xpbWl0X3JlYWNoZWSUTowPY2l0YXRpb25fY2hlY2tzlE6MD2NpdGF0aW9uX2lzc3Vlc5RLAIwRY2l0YXRpb25zX3NraXBwZWSUTnVoKH2UaCqPlChoM2g3aDZoRGhBaEloRmhCaDRoRWhIaEdoQ2g1kGgsTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoKH2UaCqPlChoUpBoLE51YowScG9sbF9hZnRlcl9zZWNvbmRzlEsKaEJdlIwIZmFpbHVyZXOUXZRoSF2UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwLbW9yZV9jbGFpbXOUXZSMFG1vcmVfY2xhaW1fbG9jYXRpb25zlF2UjA5tb3JlX2NpdGF0aW9uc5RdlIwHZmFpbHVyZZROjAZjbGFpbXOUXZQoaACMC1Jldmlld0NsYWltlJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAjAVjbGFpbZSMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lE6MCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAdydW5uaW5nlIwHdmVyZGljdJROaB9OjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBB2ZXJpZmljYXRpb25fdXJslE6MCmVycm9yX2NvZGWUTowRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTmhiTnVoKH2UaCqPlChofWh4aHloH2gLaHZoe2h3aHpofmhikGgsTnVijAplc2NhbGF0aW9ulE6MDHZlcmlmaWNhdGlvbpROjA9zdWdnZXN0ZWRfZWRpdHOUTnVoKH2UaCqPlChoa2hvaIFoamiCaINobmhtkGgsTnViaGYpgZR9lChoBX2UKGhqSwFoa4wiV2F0ZXIgYm9pbHMgYXQgMTAwIEMgYXQgc2VhIGxldmVsLpRobU5obk5ob2hxKYGUfZQoaAV9lChoC4wHcnVubmluZ5Rodk5oH05od05oeE5oeU5oek5oe12UaH1OaH5OaGJOdWgofZRoKo+UKGh9aHhoeWgfaAtodmh7aHdoemh+aGKQaCxOdWJogU5ogk5og051aCh9lGgqj5QoaGtob2iBaGpogmiDaG5obZBoLE51YmWMCWNpdGF0aW9uc5RdlHVoKH2UaCqPlChoXGhXaBBoC2gUaFpoVWgtaGBoTGgOaEhoY2gNaF5oEmgJaJNoQmgHaGKQaCxOdWIu", "review__get_assessment_rows_full_fields.json": "gASV+QwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKUmV2aWV3RnVsbJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBHZpZXeUjARmdWxslIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMCmluY29tcGxldGWUjApjcmVhdGVkX2F0lIwbMjAyNi0xMC0wMVQwOTowNzowMC4xMjM0NTZalIwMY29tcGxldGVkX2F0lIwbMjAyNi0xMC0wMVQxMDowNzowMC4xMjM0NTZalIwIbGFuZ3VhZ2WUjAJlbpSMBnBvbGljeZRoAIwQRXNjYWxhdGlvblBvbGljeZSTlCmBlH2UKGgFfZQojAh2ZXJkaWN0c5RdlCiMBUZhbHNllIwMTW9zdGx5IEZhbHNllIwFTWl4ZWSUZYwKY29uZmlkZW5jZZRdlIwDbG93lGGMD21heF9hc3Nlc3NtZW50c5RLFIwRbWF4X3ZlcmlmaWNhdGlvbnOUSwCMBWRlcHRolIwIc3RhbmRhcmSUjA1tYXhfY2l0YXRpb25zlEsAjA1zdWdnZXN0X2VkaXRzlIl1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgbaCVoIGgnaChoJGgjkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjA1SZXZpZXdTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NsYWltc19zZWxlY3RlZJRLAowLY2xhaW1fbGltaXSUSxSME2NsYWltX2xpbWl0X3JlYWNoZWSUiYwPaW5wdXRfdHJ1bmNhdGVklImMC2Fzc2Vzc21lbnRzlGgAjBZSZXZpZXdBc3Nlc3NtZW50Q291bnRzlJOUKYGUfZQoaAV9lCiMCWNvbXBsZXRlZJRLAYwGZmFpbGVklEsBdWgpfZRoK4+UKGg+aD+QaC1OdWKMDXZlcmlmaWNhdGlvbnOUaACMGFJldmlld1ZlcmlmaWNhdGlvbkNvdW50c5STlCmBlH2UKGgFfZQojAdwbGFubmVklEsAaD5LAGg/SwB1aCl9lGgrj5QoaD5oP2hIkGgtTnVijAZpc3N1ZXOUSwGMD2NpdGF0aW9uc19mb3VuZJROjBJjaXRhdGlvbnNfc2VsZWN0ZWSUTowOY2l0YXRpb25fbGltaXSUTowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJROjA9jaXRhdGlvbl9jaGVja3OUTowPY2l0YXRpb25faXNzdWVzlEsAjBFjaXRhdGlvbnNfc2tpcHBlZJROdWgpfZRoK4+UKGg0aDhoN2hNaEJoUmhPaEtoNWhOaFFoUGhMaDaQaC1OdWKMB2NyZWRpdHOUaACMDVJldmlld0NyZWRpdHOUk5QpgZR9lChoBX2UjAdjaGFyZ2VklEsCc2gpfZRoK4+UKGhbkGgtTnVijBJwb2xsX2FmdGVyX3NlY29uZHOUTmhLXZRoAIwLUmV2aWV3SXNzdWWUk5QpgZR9lChoBX2UKIwLY2xhaW1faW5kZXiUSwCMBWNsYWltlIwoVGhlIEVVIEFJIEFjdCB0b29rIGVmZmVjdCBpbiBNYXJjaCAyMDI0LpSMDnZlcmlmaWVkX2NsYWltlE6MB3ZlcmRpY3SUjAVGYWxzZZRoIIwEaGlnaJSMBnNvdXJjZZSMCmFzc2Vzc21lbnSUjA92ZXJpZmljYXRpb25faWSUTowTdmVyaWZpY2F0aW9uX3N0YXR1c5ROjBB2ZXJpZmljYXRpb25fdXJslE6MA3VybJROjAplc2NhbGF0aW9ulGgAjApFc2NhbGF0aW9ulJOUKYGUfZQoaAV9lCiMDW1hdGNoZWRfcnVsZXOUXZSMB3ZlcmRpY3SUYYwLZGlzcG9zaXRpb26UjANjYXCUdWgpfZRoK4+UKGh7aHiQaC1OdWKMC2tleV9maW5kaW5nlE6MCXJhdGlvbmFsZZSMI1R3byByZXZpZXdlcnMgZm91bmQgdGhlIGRhdGUgd3JvbmculIwRc3VnZ2VzdGVkX3Jld3JpdGWUjDBUaGUgRVUgQUkgQWN0IGVudGVyZWQgaW50byBmb3JjZSBpbiBBdWd1c3QgMjAyNC6UjAdmYWlsdXJllE6MD3N1Z2dlc3RlZF9lZGl0c5ROdWgpfZRoK4+UKGhmaG5ocGggaGlob2hyaIBohWhxaGxoaGhlaH9ogmiEkGgtTnViYYwIZmFpbHVyZXOUXZRoAIwNUmV2aWV3RmFpbHVyZZSTlCmBlH2UKGgFfZQoaGVLAWhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UjAVzdGFnZZSMCmFzc2Vzc21lbnSUaIRoAIwMRmFpbHVyZUJsb2NrlJOUKYGUfZQoaAV9lCiMDmZhaWx1cmVfcmVhc29ulIwHdGltZW91dJSMDWZhaWx1cmVfY2xhc3OUjBR1cHN0cmVhbV91bmF2YWlsYWJsZZSMCXJldHJ5YWJsZZSIjARoaW50lIw5VGhlIGNoZWNrIHJhbiBvdXQgb2YgdGltZS4gUmV0cnkgaXQ7IG5vdGhpbmcgd2FzIGNoYXJnZWQulIwIZG9jc191cmyUTnVoKX2UaCuPlChonGiXaJ5omWibkGgtTnVidWgpfZRoK4+UKGhmaGVokGiEkGgtTnViYWhRXZSMEWNpdGF0aW9uX2ZhaWx1cmVzlF2UjAttb3JlX2NsYWltc5RdlIwUbW9yZV9jbGFpbV9sb2NhdGlvbnOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaIROjAZjbGFpbXOUXZQoaACMC1Jldmlld0NsYWltlJOUKYGUfZQoaAV9lCiMBWluZGV4lEsAaGaMKFRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gTWFyY2ggMjAyNC6UjAlwb3NpdGlvbnOUTowGcmVzdWx0lGgAjAxSZXZpZXdSZXN1bHSUk5QpgZR9lChoBX2UKGhpjAVGYWxzZZRoIIwEaGlnaJRobIwKYXNzZXNzbWVudJSMCGlzX2lzc3VllIh1aCl9lGgrj5QoaCBoaWhsaL+QaC1OdWKMCmFzc2Vzc21lbnSUaACMEFJldmlld0Fzc2Vzc21lbnSUk5QpgZR9lChoBX2UKGgLjAljb21wbGV0ZWSUaGmMBUZhbHNllGggjARoaWdolGiAjCNUd28gcmV2aWV3ZXJzIGZvdW5kIHRoZSBkYXRlIHdyb25nLpSMB2Rpc3NlbnSUjDpPbmUgcmV2aWV3ZXIgcmVhZCB0aGUgY2xhaW0gYXMgYWJvdXQgdGhlIGVudHJ5IGludG8gZm9yY2UulGhwTowKZXJyb3JfY29kZZROjBFpZGVudGlmaWVkX2NsYWltc5RdlCiMIlRoZSBFVSBBSSBBY3QgdG9vayBlZmZlY3QgaW4gMjAyNC6UjBhJdCB0b29rIGVmZmVjdCBpbiBNYXJjaC6UZWicjCRUaGlzIHRleHQgaG9sZHMgbW9yZSB0aGFuIG9uZSBjbGFpbS6UaIKMMFRoZSBFVSBBSSBBY3QgZW50ZXJlZCBpbnRvIGZvcmNlIGluIEF1Z3VzdCAyMDI0LpRohE51aCl9lGgrj5QoaJxozGhwaCBoC2hpaM9ogGjOaIJohJBoLU51YmhyaHQpgZR9lChoBX2UKGh4XZSMB3ZlcmRpY3SUYWh7jANjYXCUdWgpfZRoK4+UKGh7aHiQaC1OdWKMDHZlcmlmaWNhdGlvbpROaIVOdWgpfZRoK4+UKGhmaMJocmizaN9ohWi2aLWQaC1OdWJorymBlH2UKGgFfZQoaLNLAWhmjB5UaGUgRWlmZmVsIFRvd2VyIGlzIGluIEJlcmxpbi6UaLVOaLZOaMJoxCmBlH2UKGgFfZQoaAuMBmZhaWxlZJRoaU5oIE5ogE5ozE5ocE5ozowHdGltZW91dJRoz12UaJxOaIJOaIRokymBlH2UKGgFfZQoaJeMB3RpbWVvdXSUaJmMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGibiGicjDlUaGUgY2hlY2sgcmFuIG91dCBvZiB0aW1lLiBSZXRyeSBpdDsgbm90aGluZyB3YXMgY2hhcmdlZC6UaJ5OdWgpfZRoK4+UKGicaJdonmiZaJuQaC1OdWJ1aCl9lGgrj5QoaJxozGhwaCBoC2hpaM9ogGjOaIJohJBoLU51YmhyTmjfTmiFTnVoKX2UaCuPlChoZmjCaHJos2jfaIVotmi1kGgtTnViZYwJY2l0YXRpb25zlF2UdWgpfZRoK4+UKGimaIhoEWgLaBVopGheaC5oqmhVaA9oUWisaA1oqGgTaAlo+GhLaAdohJBoLU51Yi4=", @@ -101,7 +101,7 @@ "review__receipt_202.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", "review__receipt_202_empty_webhook_url.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", "review__receipt_202_webhook_url.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", - "review__replay_later_202.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", + "review__stored_replay_202.json": "gASVtQAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNUmV2aWV3U3RhcnRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAlyZXZpZXdfaWSUjAhkNmIyYmQ3MpSMBnN0YXR1c5SMBnF1ZXVlZJR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", "verify__batch_202.json": "gASVpAEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNQmF0Y2hBY2NlcHRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAhiYXRjaF9pZJSMEDdlYTljZGJkNDA2NGUzY2aUjAVpdGVtc5RdlChoAIwMVGFza0FjY2VwdGVklJOUKYGUfZQoaAV9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjBNUaGUgRWFydGggaXMgcm91bmQulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBJoEJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgMKYGUfZQoaAV9lChoEIwgMGI5MmI0NDNlYjY5YjljNzE0MzRmZGM2NGYyY2E4YmWUaBKMIVdhdGVyIGJvaWxzIGF0IDEwMEMgYXQgc2VhIGxldmVsLpR1aBR9lGgWj5QoaBJoEJBoGE51YmV1aBR9lGgWj5QoaAdoCZBoGE51Yi4=", "verify__batch_partial_202.json": "gASVPgEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNQmF0Y2hBY2NlcHRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAhiYXRjaF9pZJSMEDdlYTljZGJkNDA2NGUzY2aUjAVpdGVtc5RdlGgAjAxUYXNrQWNjZXB0ZWSUk5QpgZR9lChoBX2UKIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwKY2xhaW1fdGV4dJSMDEZpcnN0IGNsYWltLpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgSaBCQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJhdWgUfZSMB3BhcnRpYWyUiHNoFo+UKGgHaBpoCZBoGE51Yi4=", "verify__delete_200.json": "gASVrAAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjACUjApjbGFpbV90ZXh0lGgIdYwSX19weWRhbnRpY19leHRyYV9flH2UjAJva5SIc4wXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaAyQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", @@ -114,7 +114,7 @@ "verify__list_clamped_200.json": "gASVvgAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlIwFdG90YWyUSwCMBHBhZ2WUSwGMCXBhZ2Vfc2l6ZZRLZHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaApoC2gJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", "verify__list_empty_200.json": "gASVvgAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlIwFdG90YWyUSwCMBHBhZ2WUSwGMCXBhZ2Vfc2l6ZZRLFHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaApoC2gJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", "verify__list_page2_200.json": "gASVbgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlGgAjBRWZXJpZmljYXRpb25MaXN0SXRlbZSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMCmxlbnpfc2NvcmWUSwmMC2tleV9maW5kaW5nlIwuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpSMEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoEGgOaBRoGGggaBJoFmgjaCJoHWgaaBtoH5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBXRvdGFslEsDjARwYWdllEsCjAlwYWdlX3NpemWUSwJ1aCV9lGgnj5QoaCtoLGgqaAeQaClOdWIu", - "verify__replay_later_202.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", + "verify__stored_replay_202.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", "verify__select_202.json": "gASVpAEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNQmF0Y2hBY2NlcHRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAhiYXRjaF9pZJSMEDdlYTljZGJkNDA2NGUzY2aUjAVpdGVtc5RdlChoAIwMVGFza0FjY2VwdGVklJOUKYGUfZQoaAV9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjBNUaGUgRWFydGggaXMgcm91bmQulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBJoEJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgMKYGUfZQoaAV9lChoEIwgMGI5MmI0NDNlYjY5YjljNzE0MzRmZGM2NGYyY2E4YmWUaBKMIVdhdGVyIGJvaWxzIGF0IDEwMEMgYXQgc2VhIGxldmVsLpR1aBR9lGgWj5QoaBJoEJBoGE51YmV1aBR9lGgWj5QoaAdoCZBoGE51Yi4=", "verify__status_cancelled_after_a_while.json": "gASVNgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjApDYW5jZWxsZWQulIwOZmFpbHVyZV9yZWFzb26UjAljYW5jZWxsZWSUjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMCWNhbmNlbGxlZJSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMJWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNjYW5jZWxsZWSUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", "verify__status_completed.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", @@ -130,10 +130,10 @@ "verify__status_needs_input.json": "gASVGAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAtuZWVkc19pbnB1dJSMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMBnJlYXNvbpSMC211bHRpX2NsYWltlIwEaGludJSMYFRoZSB0ZXh0IGhvbGRzIHNldmVyYWwgZGlzdGluY3QgY2xhaW1zLiBTZW5kIHRoZSBvbmVzIHRvIGNoZWNrIHRvIFBPU1QgL3ZlcmlmeS97dGFza19pZH0vc2VsZWN0LpSMCHByb2dyZXNzlGgAjAhQcm9ncmVzc5STlCmBlH2UKGgFfZQojARzdGVwlIwAlIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UKGgAjA5DYW5kaWRhdGVDbGFpbZSTlCmBlH2UKGgFfZQojAR0ZXh0lIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMBmRvbWFpbpSMB1NjaWVuY2WUdWgbfZRoHY+UKGgqaCiQaB9OdWJoJCmBlH2UKGgFfZQoaCiMIVdhdGVyIGJvaWxzIGF0IDEwMEMgYXQgc2VhIGxldmVsLpRoKowHU2NpZW5jZZR1aBt9lGgdj5QoaCpoKJBoH051YmWMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoFowOZmFpbHVyZV9yZWFzb26UaBaMDmZhaWx1cmVfZGV0YWlslGgWjA1mYWlsdXJlX2NsYXNzlGgWjAlyZXRyeWFibGWUTowIZG9jc191cmyUaBZ1aBt9lGgdj5QoaA1oC2gJaAdoIZBoH051Yi4=", "verify__status_not_a_claim.json": "gASV3wIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lIyPVGhlIGlucHV0IGlzIGEgZ3JlZXRpbmcsIG5vdCBhIHN0YXRlbWVudCB0aGF0IGNhbiBiZSBjaGVja2VkLiBTZW5kIG9uZSBmYWN0dWFsIGNsYWltLCBvciBydW4gdGhlIHRleHQgdGhyb3VnaCAvZXh0cmFjdCB0byBlbnVtZXJhdGUgaXRzIGNsYWltcy6UjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjBdOb3QgYSB2ZXJpZmlhYmxlIGNsYWltLpSMDmZhaWx1cmVfcmVhc29ulIwLbm90X2FfY2xhaW2UjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMDWludmFsaWRfaW5wdXSUjAlyZXRyeWFibGWUiYwIZG9jc191cmyUjClodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjaW52YWxpZC1pbnB1dJR1aBp9lGgcj5QoaA1oCWgHaChoLmgraC1oJpBoHk51Yi4=", "verify__status_not_a_claim_after_a_while.json": "gASVVAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjB5QaXBlbGluZSBzdG9wcGVkOiBub3RfYV9jbGFpbS6UjA5mYWlsdXJlX3JlYXNvbpSMC25vdF9hX2NsYWltlIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjA1pbnZhbGlkX2lucHV0lIwJcmV0cnlhYmxllImMCGRvY3NfdXJslIwpaHR0cHM6Ly9sZW56LmlvL2RvY3MvZXJyb3JzI2ludmFsaWQtaW5wdXSUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", - "verify__status_older_run_completed.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", - "verify__status_older_run_failed_crashed.json": "gASVOwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjBBQaXBlbGluZSBmYWlsZWQulIwOZmFpbHVyZV9yZWFzb26UjAp0YXNrX2Vycm9ylIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjAhpbnRlcm5hbJSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMJGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnRlcm5hbJR1aBl9lGgbj5QoaAloB2gnaC1oKmgsaCWQaB1OdWIu", - "verify__status_older_run_failed_insufficient_evidence.json": "gASVbAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjCNQaXBlbGluZSBzdG9wcGVkIGF0OiByZXNlYXJjaF9lbXB0eZSMDmZhaWx1cmVfcmVhc29ulIwOcmVzZWFyY2hfZW1wdHmUjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMFWluc3VmZmljaWVudF9ldmlkZW5jZZSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMMWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnN1ZmZpY2llbnQtZXZpZGVuY2WUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", - "verify__status_older_run_in_progress.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAhyZXNlYXJjaJSMBWluZGV4lEsCjAV0b3RhbJRLBYwPZWxhcHNlZF9zZWNvbmRzlEsBjBJwb2xsX2FmdGVyX3NlY29uZHOUSwV1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYaBdoFmgUaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBnJlc3VsdJROjAZjbGFpbXOUXZSMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoDIwOZmFpbHVyZV9yZWFzb26UaAyMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlGgMjAlyZXRyeWFibGWUTowIZG9jc191cmyUaAx1aBp9lGgcj5QoaA5oB2gJkGgeTnViLg==", + "verify__stored_progress_completed.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", + "verify__stored_progress_failed_crashed.json": "gASVOwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjBBQaXBlbGluZSBmYWlsZWQulIwOZmFpbHVyZV9yZWFzb26UjAp0YXNrX2Vycm9ylIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjAhpbnRlcm5hbJSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMJGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnRlcm5hbJR1aBl9lGgbj5QoaAloB2gnaC1oKmgsaCWQaB1OdWIu", + "verify__stored_progress_failed_insufficient_evidence.json": "gASVbAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjCNQaXBlbGluZSBzdG9wcGVkIGF0OiByZXNlYXJjaF9lbXB0eZSMDmZhaWx1cmVfcmVhc29ulIwOcmVzZWFyY2hfZW1wdHmUjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMFWluc3VmZmljaWVudF9ldmlkZW5jZZSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMMWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnN1ZmZpY2llbnQtZXZpZGVuY2WUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", + "verify__stored_progress_in_progress.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAhyZXNlYXJjaJSMBWluZGV4lEsCjAV0b3RhbJRLBYwPZWxhcHNlZF9zZWNvbmRzlEsBjBJwb2xsX2FmdGVyX3NlY29uZHOUSwV1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYaBdoFmgUaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBnJlc3VsdJROjAZjbGFpbXOUXZSMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoDIwOZmFpbHVyZV9yZWFzb26UaAyMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlGgMjAlyZXRyeWFibGWUTowIZG9jc191cmyUaAx1aBp9lGgcj5QoaA5oB2gJkGgeTnViLg==", "verify__status_processing.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAhyZXNlYXJjaJSMBWluZGV4lEsCjAV0b3RhbJRLBYwPZWxhcHNlZF9zZWNvbmRzlEsBjBJwb2xsX2FmdGVyX3NlY29uZHOUSwV1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYaBdoFmgUaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBnJlc3VsdJROjAZjbGFpbXOUXZSMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoDIwOZmFpbHVyZV9yZWFzb26UaAyMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlGgMjAlyZXRyeWFibGWUTowIZG9jc191cmyUaAx1aBp9lGgcj5QoaA5oB2gJkGgeTnViLg==", "verify__status_processing_after_a_while.json": "gASVCgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAxhZGp1ZGljYXRpb26UjAVpbmRleJRLBIwFdG90YWyUSwWMD2VsYXBzZWRfc2Vjb25kc5RLAYwScG9sbF9hZnRlcl9zZWNvbmRzlEsFdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgXaBZoFGgZkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgafZRoHI+UKGgOaAdoCZBoHk51Yi4=", "verify__status_task_stuck.json": "gASVewIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjDhUaGUgdGFzayB3YXMgbmV2ZXIgY29tcGxldGVkIGFuZCBoYXMgYmVlbiBtYXJrZWQgZmFpbGVkLpSMDmZhaWx1cmVfcmVhc29ulIwKdGFza19zdHVja5SMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlIwUdXBzdHJlYW1fdW5hdmFpbGFibGWUjAlyZXRyeWFibGWUiIwIZG9jc191cmyUjDBodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjdXBzdHJlYW0tdW5hdmFpbGFibGWUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", diff --git a/tests/parity_observe.py b/tests/parity_observe.py index 4852e3c..8211522 100644 --- a/tests/parity_observe.py +++ b/tests/parity_observe.py @@ -166,11 +166,15 @@ def _model_for(name: str) -> Any: if area == "account" and case.startswith("library"): return models.LibraryList if area == "review": - return models.ReviewStarted if case.startswith(("receipt", "idempotent", "replay")) else models.ReviewFull + return ( + models.ReviewStarted + if case.startswith(("receipt", "idempotent", "replay", "stored_replay")) + else models.ReviewFull + ) if area == "citecheck": return models.CitecheckStarted if case.startswith(("receipt", "idempotent")) else models.Citecheck if area == "verify": - if case.startswith("status_"): + if case.startswith(("status_", "stored_progress")): return models.TaskStatus if case.startswith(("batch_", "select_")): return models.BatchAccepted diff --git a/tests/test_api_version_guard.py b/tests/test_api_version_guard.py index 1dc5715..fe2230a 100644 --- a/tests/test_api_version_guard.py +++ b/tests/test_api_version_guard.py @@ -101,14 +101,24 @@ def test_the_current_version_and_a_missing_header_proceed(client: Lenz) -> None: assert client.usage().plan -def test_a_stored_replay_in_the_old_shape_is_refused(client: Lenz) -> None: +@pytest.mark.parametrize( + ("call", "name"), + [ + (lambda c: c.verify("A.", idempotency_key="k"), "verify__stored_replay_202.json"), + (lambda c: c.assess("A.", idempotency_key="k"), "assess__stored_replay_200.json"), + (lambda c: c.extract(text="A.", idempotency_key="k"), "extract__stored_replay_200.json"), + (lambda c: c.review("A.", idempotency_key="k"), "review__stored_replay_202.json"), + ], +) +def test_a_stored_replay_in_the_old_shape_is_refused(client: Lenz, call: Any, name: str) -> None: """The API answers a replay of an idempotent request stored by an older release in the old shape, and says so in the header.""" - fixture = load("legacy", "assess__replay_later_200.json") + fixture = load("legacy", name) + path = "/" + name.split("__", 1)[0] with respx.mock(base_url=DEFAULT_BASE_URL) as mock: - mock.post("/assess").respond(fixture["status"], json=fixture["body"], headers={"X-Lenz-API-Version": OLD}) + mock.post(path).respond(fixture["status"], json=fixture["body"], headers={"X-Lenz-API-Version": OLD}) with pytest.raises(LenzApiVersionError) as info: - client.assess("A.", idempotency_key="k") + call(client) assert info.value.api_version == OLD assert info.value.body == fixture["body"] From 05988362ed1bdc4b04ce71ee4fc69bc7f5c3d1c8 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 12:59:52 +0300 Subject: [PATCH 18/67] docs: split the 3.0 migration into required and optional, document the version guard List the values the API no longer sends as breaking, say truthfully what model_dump() and --json return (the 2.x-compatible fields plus the keys the server sent; exc.body and event.raw are the body as sent), document LenzApiVersionError, and name the replacements for AssessClaim.hint. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 113 +++++++++++++++++++++++++++++++++++---------------- README.md | 39 +++++++++++++++--- 2 files changed, 110 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e163b4..ca04e74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,20 +6,41 @@ All notable changes to this SDK are documented here. Format follows ## [Unreleased] -Major release (3.0.0). Every attribute, exception and CLI rendering reads -what 2.x read, with one exception: the raw bodies (`exc.body`, -`model_dump()`, a webhook event's `raw`, the CLI's `--json` output) show the -API's current response shape as sent. Every 2.x name is kept as a deprecated -alias (see Deprecated); see the migration note below. +Major release (3.0.0). The SDK asks for the API's current response shape +(`2026-10-11`) and reads only that shape from its own calls. Every attribute, +exception and CLI rendering keeps the value it had in 2.x (computed from the +current shape), with the exceptions listed under Breaking and "What reads +differently". The raw bodies are the current shape: `exc.body` and a webhook +event's `raw` hold the body as sent; `model_dump()` and the CLI's `--json` +hold the 2.x-compatible fields plus the current-shape keys the server sent +(see Migration). Every 2.x name is kept as a deprecated alias (see +Deprecated). ### Breaking - **3.0 reads only the API's `2026-10-11` response shape for its own calls** - (lenz.io serves it from 2026-10-11). Run against a server that does not - serve that version, it does not read the 2.x (`2026-05-13`) response - shape any more: stay on 2.21 until the API serves `2026-10-11`. The SDK no - longer detects or fills in a 2.x-shaped response body, a 2.x-shaped error - body, or a `/me/usage` body from before the credit pool. + (lenz.io serves it from 2026-10-11). The SDK no longer detects or fills in + a 2.x-shaped response body, a 2.x-shaped error body, or a `/me/usage` body + from before the credit pool. +- **`LenzApiVersionError` (new, a `LenzError`) is raised when a response + names another version** in its `X-Lenz-API-Version` header, in practice + `2026-05-13`: a server still on the older version, or a reply replayed from + an idempotent request stored before the change. It carries `api_version`, + `status_code` and `body` (as sent), applies to success and error responses + of client calls, and never to webhook payloads. A response without the + header is read as usual. Set the API version on the server side to + `2026-10-11`, or pin `lenz-io<3`. +- **Values the API no longer sends**, which no client can rebuild: + - `TaskAccepted.chain_id` reads `""` (use `task_id`). + - The `task_id` of a `review.*` or `citecheck.*` webhook event is the + review or citation-check id (use `event_id` to deduplicate, and the + event's `review_id` / `citecheck_id`); these ids were never pollable. + - Some failure sentences and hints are worded anew (listed under "What + reads differently"); compare on `failure.code` / `code`, never on the + text. + - An `/extract` on an input the API first read as not a claim and then + found a claim in says `status == "ready"` (2.x: `"not_a_claim"`); branch + on `claims`. - **Webhooks of both shapes are still parsed** (`parse_webhook`, `LenzWebhooks.parse` and the event models): work submitted by an older client on the same account is delivered in the 2.x shape. @@ -40,8 +61,11 @@ alias (see Deprecated); see the migration note below. - "Nothing checkable" keeps each field's old spelling: `no_claim` on `/assess` rows, `AssessResponse.error_code`, review rows and the review's own failure; `not_a_claim` on `/extract`, a verification - (`TaskStatus.failure_reason`, `LenzPipelineError.failure_reason`, the - `verification.failed` webhook's `error`) and a deep check inside a review. + (`TaskStatus.failure_reason` and the `failure_reason` of its `failure` + block, `LenzPipelineError.failure_reason`, the `verification.failed` + webhook's `error` and the `failure_reason` of its `failure` block) and a + deep check inside a review. `failure.code` is `no_checkable_claim` + everywhere. - `modified_at` (set only when a verification completed on a later UTC calendar day than it was created), `claim_text` on receipts, `text` on `needs_input` options, `ExtractedClaims.claim` / `identified_claims` / @@ -87,9 +111,9 @@ now a declared dependency (pydantic already installs it). | `ExtractedClaims.candidate_claims`, `AssessClaim.candidate_claims`, `AssessResponse.candidate_claims`, `TaskStatus.candidates`, `TaskStatus.similar_claims` | none: always empty | | `AssessClaim.verdict == "Error"` (with `confidence == "low"`) | `status == "failed"` | | `AssessClaim.error_code` | `failure.code` (`no_checkable_claim` where it reads `no_claim`) | -| `AssessClaim.hint` | `failure.hint`; none for the sentence on a completed row that found other claims | +| `AssessClaim.hint` | on a failed row `failure.hint`; on a completed row that found other claims, `more_claims` (the sentence is no longer sent; the attribute still reads it) | | `AssessClaim.identified_claims`, `ReviewAssessment.identified_claims` | `more_claims` | -| `ReviewAssessment.error_code`, `ReviewAssessment.hint` | `failure.code`, `failure.hint` | +| `ReviewAssessment.error_code`, `ReviewAssessment.hint` | `failure.code`, `failure.hint` on a failed quick check; `more_claims` on a completed one that found other claims (`hint` still reads "This text holds more than one claim.") | | `AssessResponse.error`, `AssessResponse.error_code` | `status` and `failure` | | `CandidateClaim.text` | `claim` | | `TaskAccepted.claim_text`, `BatchItemResult.claim_text` | `claim` | @@ -142,8 +166,8 @@ rebuild: - Reviews: a review row stored without a failure block (a quick-check row or a `ReviewFailure`) reads one, where 2.x read `failure` `None`; a deep check's `modified_at` is computed from its completion time by the 2.x rule - instead of read as stored; a quick-check row whose passage held several - claims has no `hint`. + instead of read as stored; a completed quick-check row that found other claims reads the fixed 2.x + `hint` ("This text holds more than one claim."); a failed one has none. - An extraction the API first read as not a claim and then found one in says `status == "ready"` (2.x: `"not_a_claim"`). - `verify`'s receipt has no `chain_id` (`TaskAccepted.chain_id` reads `""`); @@ -153,28 +177,45 @@ rebuild: ### Migration -- **Webhooks follow the version of the request that submitted the work.** A - `verify`, `verify_batch`, `select`, `review` or `citecheck` call made with - 3.0 gets its webhooks in the current shape (one envelope: `event`, - `event_id`, the work's id, `status`, and the polled body under - `verification` / `review` / `citecheck`). If the service that RECEIVES your - webhooks parses them with lenz-io 2.20 or older, or reads the raw JSON, - upgrade it to 2.21 or later (which reads both shapes) before the service - that SENDS requests moves to 3.0. Work submitted with 2.x keeps sending - the original shape. -- Code that reads attributes: nothing to do. The deprecated names above keep - working; the SDK needs an API that serves `2026-10-11` (see Breaking). -- Code that reads the raw dict: the keys are the current shape's. A model's - `model_dump()` holds what the server sent plus the 2.x fields filled in from - it; `exc.body` and `event.raw` hold the body as sent (`failure` blocks, - `claims` lists, `completed_at`, `more_claims`, `docs_url`, `retry_after`). - Read the attributes instead, or stay on 2.21 until you move. +**Required** + +- **Webhook receivers.** Webhooks follow the version of the request that + submitted the work: a `verify`, `verify_batch`, `select`, `review` or + `citecheck` call made with 3.0 gets its webhooks in the current shape (one + envelope: `event`, `event_id`, the work's id, `status`, and the polled body + under `verification` / `review` / `citecheck`). A service that RECEIVES + your webhooks and parses them with lenz-io 2.20 or older, or reads the raw + JSON, must be upgraded to 2.21 or later (which reads both shapes) before + the service that SENDS requests moves to 3.0. Work submitted with 2.x keeps + sending the original shape. +- **The API must answer `2026-10-11`.** 3.0 raises `LenzApiVersionError` + from any call whose response names another version; until the API serves + `2026-10-11` for your account, stay on 2.21. +- **Code that reads raw bodies**: `exc.body` and `event.raw` are the body as + sent, in the current shape (`failure` blocks, `claims` lists, + `completed_at`, `more_claims`, `docs_url`, `retry_after`); read the + attributes instead, or stay on 2.21 until you move. +- **Tests with recorded 2.x response bodies** must be re-recorded: the SDK + reads the current shape only, so a stored 2.x body no longer parses into + the values it did. +- **The values the API no longer sends** (see Breaking): `chain_id`, the + review / citecheck webhook `task_id`, reworded failure sentences and hints, + and an `/extract` status on a mixed input. + +**Optional** + +- Move off the deprecated names (see Deprecated): they keep working with + their 2.x values, so nothing breaks if you do not. +- `model_dump()` and the CLI's `--json` return the 2.x-compatible fields + (computed from the response) plus the current-shape keys the server sent; + they are not the wire body. If a script parses `--json` output, it keeps + finding every 2.x key and gains the new ones. - `webhook_url` keeps its meaning: on `verify` and `verify_batch` an empty or whitespace-only value (the default is `""`) means your key's default - webhook and is not sent; on - `review` and `citecheck`, `None` means your key's default and `""` means no - webhook. In the current shape the API reads a missing `webhook_url` as the - key's default and `""` as no webhook on every endpoint. + webhook and is not sent; on `review` and `citecheck`, `None` means your + key's default and `""` means no webhook. In the current shape the API reads + a missing `webhook_url` as the key's default and `""` as no webhook on + every endpoint. ## [2.21.0] - 2026-10-09 diff --git a/README.md b/README.md index e2dee87..78ad521 100644 --- a/README.md +++ b/README.md @@ -477,9 +477,10 @@ The API's current response shape gives each field one name across every endpoint. Since 3.0 the SDK asks for it (`X-Lenz-API-Version: 2026-10-11`) and reads only that shape from its own calls (webhooks of both shapes are still parsed). The current names are attributes already. The 2.x names are -**deprecated** and still work, with the value they had in 2.x, so 2.x code -runs unchanged; they will be removed in a future major release. Move to the -current names when convenient: +**deprecated** and still work, with the value they had in 2.x (except the few +values the API no longer sends, listed under "What reads differently" in the +[changelog](CHANGELOG.md)); they will be removed in a future major release. +Move to the current names when convenient: | Read this | Instead of (deprecated, still works) | |---|---| @@ -496,9 +497,15 @@ current names when convenient: "Nothing checkable" is `no_checkable_claim` in the current names; the 2.x fields keep their own spelling (`not_a_claim`, `no_claim`). The current names -are read-only properties, so the 2.x fields parse, dump and compare as they -did. The full list, with the aliases that have no replacement, is in the -[changelog](CHANGELOG.md). +are read-only properties. The full list, with the aliases that have no +replacement, is in the [changelog](CHANGELOG.md). + +**What `model_dump()` and `--json` return.** Not the response body as sent: +a model's `model_dump()` (and the CLI's `--json`, which prints it) holds the +2.x-compatible fields, computed from the response, plus the current-shape +keys the server sent (`failure`, `more_claims`, `claims`, `completed_at`, ...). +The body exactly as sent is `exc.body` on an error and `event.raw` on a +webhook event. ### A suggested rewrite (`suggested_rewrite`) @@ -761,6 +768,26 @@ except LenzUpstreamUnavailableError as exc: schedule_retry_in(exc.retry_after) # typically 90-120s ``` +`LenzApiVersionError` (a `LenzError`) is raised when a response names an API +version other than the one this SDK reads. Every response carries the version +that served it in `X-Lenz-API-Version`; lenz-io 3.x asks for `2026-10-11` and +reads that version's shape only, so an answer in `2026-05-13` (a server still +on the older version, or a reply replayed from an idempotent request stored +before the change) is refused rather than misread. It carries `api_version` +(what the response named), `status_code` and `body` as sent. Set the API +version on the server side to `2026-10-11`, or pin `lenz-io<3` to keep reading +the older version. A response without the header is read as usual, and webhook +payloads are never refused (they are parsed in either shape). + +```python +from lenz_io import LenzApiVersionError + +try: + client.assess("The Earth is round.") +except LenzApiVersionError as exc: + print(exc.api_version) # "2026-05-13" +``` + A failed *verification* (as opposed to a failed HTTP call) raises `LenzPipelineError` from `verify_and_wait` / `wait`. Since 2.8.0 it carries `failure_class` (closed set: `upstream_unavailable` | `insufficient_evidence` From 9e91fc44288b82071e062c398a87d9ae8fd1ef3c Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 13:02:20 +0300 Subject: [PATCH 19/67] fix: LenzApiVersionError suggests what a caller can actually do The API version a response is served in is not a setting a caller can change; the error now says to contact support if it persists, and that 2.x reads both versions. Same wording in both SDKs. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++-- README.md | 5 ++--- src/lenz_io/client.py | 10 +++++----- tests/test_api_version_guard.py | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca04e74..b3a1f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,8 +28,8 @@ Deprecated). an idempotent request stored before the change. It carries `api_version`, `status_code` and `body` (as sent), applies to success and error responses of client calls, and never to webhook payloads. A response without the - header is read as usual. Set the API version on the server side to - `2026-10-11`, or pin `lenz-io<3`. + header is read as usual. If it persists, contact support with the request + id; lenz-io 2.x reads both versions. - **Values the API no longer sends**, which no client can rebuild: - `TaskAccepted.chain_id` reads `""` (use `task_id`). - The `task_id` of a `review.*` or `citecheck.*` webhook event is the diff --git a/README.md b/README.md index 78ad521..8b238dc 100644 --- a/README.md +++ b/README.md @@ -774,9 +774,8 @@ that served it in `X-Lenz-API-Version`; lenz-io 3.x asks for `2026-10-11` and reads that version's shape only, so an answer in `2026-05-13` (a server still on the older version, or a reply replayed from an idempotent request stored before the change) is refused rather than misread. It carries `api_version` -(what the response named), `status_code` and `body` as sent. Set the API -version on the server side to `2026-10-11`, or pin `lenz-io<3` to keep reading -the older version. A response without the header is read as usual, and webhook +(what the response named), `status_code` and `body` as sent. If it persists, +contact support with the request id; lenz-io 2.x reads both versions. A response without the header is read as usual, and webhook payloads are never refused (they are parsed in either shape). ```python diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index cd7fe63..e2efe15 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -1953,12 +1953,12 @@ def _check_served_version(response: httpx.Response) -> None: except ValueError: parsed = None raise LenzApiVersionError( - message=( - f"The API answered in version {served}; lenz-io 3.x reads {API_VERSION} only. " - f"Change the API version setting on the server side to {API_VERSION}, or pin lenz-io<3." - ), + message=f"The API answered in version {served}; lenz-io 3.x reads {API_VERSION} only.", cause=f"The response carries {_VERSION_HEADER}: {served}.", - fix=(f"Set the account's API version to {API_VERSION}, or install lenz-io<3 to keep reading {served}."), + fix=( + "If this persists, contact support (https://lenz.io/contact) with the request id; " + "lenz-io 2.x reads both versions." + ), doc_url="https://lenz.io/docs/errors", request_id=response.headers.get("X-Request-ID") or "", status_code=response.status_code, diff --git a/tests/test_api_version_guard.py b/tests/test_api_version_guard.py index fe2230a..150911e 100644 --- a/tests/test_api_version_guard.py +++ b/tests/test_api_version_guard.py @@ -71,7 +71,7 @@ def test_an_answer_in_another_version_raises(client: Lenz) -> None: assert exc.status_code == 200 assert exc.body == body assert OLD in exc.message and "2026-10-11" in exc.message - assert "lenz-io<3" in exc.message + assert "lenz-io 2.x reads both versions" in exc.fix def test_an_error_answered_in_another_version_raises_the_version_error(client: Lenz) -> None: From 1bc6059eebe37b0542483d1c85da7036117692f0 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 13:06:07 +0300 Subject: [PATCH 20/67] fix: a version error is never swallowed by a 404 shortcut or a poll An idempotent delete, the CLI's 404 fallbacks, the verification poll and an unreadable body could each turn LenzApiVersionError into success, a retry or another error. Each now lets it through. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/cli/commands.py | 4 ++-- src/lenz_io/cli/verify.py | 4 ++-- src/lenz_io/client.py | 5 +++-- tests/test_api_version_guard.py | 9 +++++++++ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/lenz_io/cli/commands.py b/src/lenz_io/cli/commands.py index 6f4b109..095c360 100644 --- a/src/lenz_io/cli/commands.py +++ b/src/lenz_io/cli/commands.py @@ -12,7 +12,7 @@ import typer from lenz_io import Lenz -from lenz_io.errors import LenzError, LenzVerificationNotReadyError +from lenz_io.errors import LenzApiVersionError, LenzError, LenzVerificationNotReadyError from ._run import execute, read_text_arg from .config import ENV_API_KEY, clear_api_key, config_path, mask_key, save_api_key @@ -100,7 +100,7 @@ def work(client: Lenz) -> None: with out.working("Thinking…"): reply = client.ask.send(verification_id, message=message) except LenzError as exc: - if exc.status_code == 404: + if exc.status_code == 404 and not isinstance(exc, LenzApiVersionError): raise CLIError( f"No verification found for id {verification_id!r}. " "A verification_id is the 8-character id printed by `lenz verify` " diff --git a/src/lenz_io/cli/verify.py b/src/lenz_io/cli/verify.py index 465e44b..fabdbcc 100644 --- a/src/lenz_io/cli/verify.py +++ b/src/lenz_io/cli/verify.py @@ -27,7 +27,7 @@ import typer from lenz_io import Lenz -from lenz_io.errors import LenzError, LenzGoneError +from lenz_io.errors import LenzApiVersionError, LenzError, LenzGoneError from lenz_io.models import TaskStatus from ._run import execute, read_text_arg @@ -413,7 +413,7 @@ def _resume( try: st = client.get_status(ident) except LenzError as exc: - if exc.status_code == 404: + if exc.status_code == 404 and not isinstance(exc, LenzApiVersionError): _resume_as_verification(client, out, ident) return raise diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index e2efe15..d2edf32 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -364,8 +364,9 @@ def delete(self, verification_id: str) -> bool: except LenzError as exc: # Idempotent DELETE: if the row was already gone (e.g. previous # request succeeded but the network reply was lost), treat as - # success rather than surfacing a confusing 404. - if exc.status_code == 404: + # success rather than surfacing a confusing 404. A 404 in another + # API version is not read as this one's. + if exc.status_code == 404 and not isinstance(exc, LenzApiVersionError): return True raise diff --git a/tests/test_api_version_guard.py b/tests/test_api_version_guard.py index 150911e..4dfb481 100644 --- a/tests/test_api_version_guard.py +++ b/tests/test_api_version_guard.py @@ -84,6 +84,15 @@ def test_an_error_answered_in_another_version_raises_the_version_error(client: L assert info.value.body == body +def test_a_404_delete_in_another_version_is_not_read_as_already_deleted(client: Lenz) -> None: + with respx.mock(base_url=DEFAULT_BASE_URL) as mock: + mock.delete("/verifications/v1").respond( + 404, json={"detail": "Not found."}, headers={"X-Lenz-API-Version": OLD} + ) + with pytest.raises(LenzApiVersionError): + client.verifications.delete("v1") + + def test_a_non_json_answer_in_another_version_still_raises(client: Lenz) -> None: with respx.mock(base_url=DEFAULT_BASE_URL) as mock: mock.get("/me/usage").respond(200, content=b"", headers={"X-Lenz-API-Version": OLD}) From 2c14539c38dde2c784ddb941ecbead10bb034147 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 13:56:10 +0300 Subject: [PATCH 21/67] test: freeze the request bodies of every public call before the 3.0 signature changes Each call is sent with its options left out, empty / zero / false and set, under a pinned Idempotency-Key, through the public methods, and the body is compared key order included: a changed body turns an idempotent replay into a 422. Co-Authored-By: Claude Opus 5.5 --- tests/test_public_call_bodies.py | 388 +++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 tests/test_public_call_bodies.py diff --git a/tests/test_public_call_bodies.py b/tests/test_public_call_bodies.py new file mode 100644 index 0000000..faa12bf --- /dev/null +++ b/tests/test_public_call_bodies.py @@ -0,0 +1,388 @@ +"""The request bodies of the public calls, frozen. + +A request body is part of the contract twice over: the server reads it, and +an ``Idempotency-Key`` replay hashes it, so a body that changes shape between +releases turns a retried request into a 422 (``idempotency_body_mismatch``) +instead of a replay. These tests send each public call with every option +left out, set to its empty / zero / false value, and set to a real value, +always under a pinned key, and compare the body that reached the wire, key +order included, with the one this release sends. + +They go through the public methods (never the private submit helpers), so a +change to a signature that forwards an option differently fails here. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest +import respx + +from lenz_io import Lenz + +BASE = "https://lenz.io/api/v1" +KEY = "pinned-key-1" +FIXTURES = Path(__file__).parent / "fixtures" / "contract" + + +def _load(name: str) -> dict[str, Any]: + return json.loads((FIXTURES / name).read_text()) + + +REVIEW_DONE = _load("review_completed.json") +CHECK_DONE = _load("citecheck_completed.json") +_COMPLETED_STATUS = {"status": "completed", "task_id": "t", "result": {"verification_id": "v1", "claim": "A."}} + + +def _ordered(content: bytes) -> list[tuple[str, Any]]: + """The body as (key, value) pairs in wire order, nested objects too.""" + return json.loads(content, object_pairs_hook=list) + + +@pytest.fixture() +def no_sleep(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: None) + + +# ── verify / verify_and_wait ─────────────────────────────────────────────── + +_VERIFY_CASES: list[tuple[str, dict[str, Any], list[tuple[str, Any]]]] = [ + ("omitted", {}, [("text", "A."), ("source_url", "")]), + ( + "empty", + {"language": "", "visibility": "", "depth": "", "source_url": "", "webhook_url": ""}, + [("text", "A."), ("source_url", "")], + ), + ("blank webhook", {"webhook_url": " "}, [("text", "A."), ("source_url", "")]), + ( + "set", + { + "language": "es", + "visibility": "unlisted", + "depth": "low", + "source_url": "https://example.com/a", + "webhook_url": "https://example.com/hook", + }, + [ + ("text", "A."), + ("source_url", "https://example.com/a"), + ("webhook_url", "https://example.com/hook"), + ("language", "es"), + ("visibility", "unlisted"), + ("depth", "low"), + ], + ), +] + + +class TestVerify: + @pytest.mark.parametrize(("label", "kwargs", "expected"), _VERIFY_CASES) + def test_verify(self, client: Lenz, label: str, kwargs: dict[str, Any], expected: list[Any]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify").respond(200, json={"task_id": "t"}) + client.verify("A.", idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + @pytest.mark.parametrize(("label", "kwargs", "expected"), _VERIFY_CASES) + def test_verify_and_wait(self, client: Lenz, label: str, kwargs: dict[str, Any], expected: list[Any]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify").respond(200, json={"task_id": "t"}) + r.get("/verify/status/t").respond(200, json=_COMPLETED_STATUS) + client.verify_and_wait("A.", idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + def test_text_alias(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify").respond(200, json={"task_id": "t"}) + client.verify(text="A.", idempotency_key=KEY) + assert _ordered(route.calls.last.request.content) == [("text", "A."), ("source_url", "")] + + +# ── verify_batch / verify_batch_and_wait ─────────────────────────────────── + +_BATCH_CASES: list[tuple[str, dict[str, Any], list[tuple[str, Any]]]] = [ + ("omitted", {}, [("claims", [[("text", "A.")], [("text", "B.")]])]), + ( + "empty", + {"webhook_url": "", "language": "", "visibility": "", "depth": ""}, + [("claims", [[("text", "A.")], [("text", "B.")]])], + ), + ( + "set", + {"webhook_url": "https://example.com/hook", "language": "de", "visibility": "unlisted", "depth": "low"}, + [ + ("claims", [[("text", "A.")], [("text", "B.")]]), + ("webhook_url", "https://example.com/hook"), + ("language", "de"), + ("visibility", "unlisted"), + ("depth", "low"), + ], + ), +] +_BATCH_ACCEPTED = {"batch_id": "b", "items": [{"task_id": "t", "claim": "A."}]} + + +class TestVerifyBatch: + @pytest.mark.parametrize(("label", "kwargs", "expected"), _BATCH_CASES) + def test_verify_batch(self, client: Lenz, label: str, kwargs: dict[str, Any], expected: list[Any]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify/batch").respond(200, json=_BATCH_ACCEPTED) + client.verify_batch(claims=[{"claim": "A."}, {"text": "B."}], idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + @pytest.mark.parametrize(("label", "kwargs", "expected"), _BATCH_CASES) + def test_verify_batch_and_wait(self, client: Lenz, label: str, kwargs: dict[str, Any], expected: list[Any]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify/batch").respond(200, json=_BATCH_ACCEPTED) + r.get("/verify/status/t").respond(200, json=_COMPLETED_STATUS) + client.verify_batch_and_wait(claims=[{"claim": "A."}, {"text": "B."}], idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + def test_item_keys_pass_through_and_a_blank_item_webhook_is_left_out(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify/batch").respond(200, json=_BATCH_ACCEPTED) + client.verify_batch( + claims=[ + {"claim": "A.", "language": "fr", "depth": "low", "webhook_url": ""}, + {"text": "B.", "visibility": "unlisted", "webhook_url": "https://example.com/h"}, + ], + idempotency_key=KEY, + ) + assert _ordered(route.calls.last.request.content) == [ + ( + "claims", + [ + [("language", "fr"), ("depth", "low"), ("text", "A.")], + [("text", "B."), ("visibility", "unlisted"), ("webhook_url", "https://example.com/h")], + ], + ) + ] + + +# ── assess / extract / select / ask.send ─────────────────────────────────── + + +class TestSyncCalls: + @pytest.mark.parametrize( + ("kwargs", "expected"), + [ + ({"claim": "A."}, [("text", "A.")]), + ({"text": "A.", "language": "", "suggest_rewrite": False}, [("text", "A.")]), + ( + {"claim": "A.", "language": "auto", "suggest_rewrite": True}, + [("text", "A."), ("language", "auto"), ("suggest_rewrite", True)], + ), + ({"claims": ["A.", "B."]}, [("claims", ["A.", "B."])]), + ({"claims": []}, [("claims", [])]), + ({"claims": ["A."], "language": "es"}, [("claims", ["A."]), ("language", "es")]), + ], + ) + def test_assess(self, client: Lenz, kwargs: dict[str, Any], expected: list[Any]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/assess").respond(200, json={"claims": []}) + client.assess(idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + @pytest.mark.parametrize( + ("kwargs", "expected"), + [ + ({}, [("text", "Doc.")]), + ({"language": "", "focus": "", "locate": None}, [("text", "Doc.")]), + ({"locate": False}, [("text", "Doc."), ("locate", False)]), + ( + {"language": "it", "focus": "figures", "locate": True}, + [("text", "Doc."), ("language", "it"), ("focus", "figures"), ("locate", True)], + ), + ], + ) + def test_extract(self, client: Lenz, kwargs: dict[str, Any], expected: list[Any]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/extract").respond(200, json={"claims": [], "status": "ok"}) + client.extract(text="Doc.", idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + @pytest.mark.parametrize( + "kwargs", + [{"claims": ["A.", "B."]}, {"texts": ["A.", "B."]}, {"claims": ["A.", "B."], "texts": ["C."]}], + ) + def test_select(self, client: Lenz, kwargs: dict[str, Any]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify/t/select").respond(200, json={"items": []}) + client.select("t", idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == [("texts", ["A.", "B."])] + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + @pytest.mark.parametrize( + ("kwargs", "expected"), + [ + ({}, [("message", "Why?")]), + ({"language": ""}, [("message", "Why?")]), + ({"language": "auto"}, [("message", "Why?"), ("language", "auto")]), + ], + ) + def test_ask_send(self, client: Lenz, kwargs: dict[str, Any], expected: list[Any]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/ask/v1").respond(200, json={"role": "expert", "content": "Because."}) + client.ask.send("v1", message="Why?", idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + +# ── review / review_and_wait ─────────────────────────────────────────────── + +_REVIEW_CASES: list[tuple[str, dict[str, Any], list[tuple[str, Any]]]] = [ + ("omitted", {}, [("text", "Draft."), ("visibility", "private")]), + ( + "empty, zero and false", + { + "verdicts": [], + "confidence": [], + "max_assessments": 0, + "max_verifications": 0, + "depth": None, + "max_citations": 0, + "suggest_edits": False, + "language": "", + "webhook_url": "", + "visibility": "", + }, + [ + ("text", "Draft."), + ("webhook_url", ""), + ("escalate", [("verdicts", []), ("confidence", []), ("max_assessments", 0), ("max_verifications", 0)]), + ], + ), + ( + "max_citations None", + {"max_citations": None, "webhook_url": None}, + [("text", "Draft."), ("visibility", "private")], + ), + ( + "set", + { + "verdicts": ["False"], + "confidence": ["low", "medium"], + "max_assessments": 5, + "max_verifications": 2, + "depth": "low", + "max_citations": 3, + "suggest_edits": True, + "language": "nl", + "webhook_url": "https://example.com/hook", + "visibility": "unlisted", + }, + [ + ("text", "Draft."), + ("language", "nl"), + ("webhook_url", "https://example.com/hook"), + ("visibility", "unlisted"), + ( + "escalate", + [ + ("verdicts", ["False"]), + ("confidence", ["low", "medium"]), + ("max_assessments", 5), + ("max_verifications", 2), + ("depth", "low"), + ("max_citations", 3), + ("suggest_edits", True), + ], + ), + ], + ), +] + + +class TestReview: + @pytest.mark.parametrize(("label", "kwargs", "expected"), _REVIEW_CASES) + def test_review(self, client: Lenz, label: str, kwargs: dict[str, Any], expected: list[Any]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/review").respond(202, json={"review_id": REVIEW_DONE["review_id"], "status": "queued"}) + client.review("Draft.", idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + @pytest.mark.parametrize(("label", "kwargs", "expected"), _REVIEW_CASES) + def test_review_and_wait( + self, client: Lenz, no_sleep: None, label: str, kwargs: dict[str, Any], expected: list[Any] + ) -> None: + rid = REVIEW_DONE["review_id"] + with respx.mock(base_url=BASE) as r: + route = r.post("/review").respond(202, json={"review_id": rid, "status": "queued"}) + r.get(f"/reviews/{rid}").respond(200, json=REVIEW_DONE) + client.review_and_wait("Draft.", idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + +# ── citecheck / citecheck_and_wait ───────────────────────────────────────── + +_PAIR = {"statement": "Water boils at 100 C.", "url": "https://example.com/boil"} +_CITECHECK_CASES: list[tuple[str, tuple[Any, ...], dict[str, Any], list[tuple[str, Any]]]] = [ + ("text omitted", ("Draft.",), {}, [("text", "Draft.")]), + ( + "text zero and empty", + ("Draft.",), + {"max_citations": 0, "language": "", "webhook_url": ""}, + [("text", "Draft."), ("max_citations", 0), ("webhook_url", "")], + ), + ( + "text set", + ("Draft.",), + {"max_citations": 4, "language": "fr", "webhook_url": "https://example.com/hook"}, + [("text", "Draft."), ("max_citations", 4), ("language", "fr"), ("webhook_url", "https://example.com/hook")], + ), + ( + "pairs", + (), + {"pairs": [_PAIR], "language": "", "webhook_url": None}, + [("pairs", [[("statement", "Water boils at 100 C."), ("url", "https://example.com/boil")]])], + ), + ( + "blank text with pairs", + (" ",), + {"pairs": [_PAIR]}, + [("pairs", [[("statement", "Water boils at 100 C."), ("url", "https://example.com/boil")]])], + ), +] + + +class TestCitecheck: + @pytest.mark.parametrize(("label", "args", "kwargs", "expected"), _CITECHECK_CASES) + def test_citecheck( + self, client: Lenz, label: str, args: tuple[Any, ...], kwargs: dict[str, Any], expected: list[Any] + ) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/citecheck").respond( + 202, json={"citecheck_id": CHECK_DONE["citecheck_id"], "status": "queued"} + ) + client.citecheck(*args, idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + @pytest.mark.parametrize(("label", "args", "kwargs", "expected"), _CITECHECK_CASES) + def test_citecheck_and_wait( + self, + client: Lenz, + no_sleep: None, + label: str, + args: tuple[Any, ...], + kwargs: dict[str, Any], + expected: list[Any], + ) -> None: + cid = CHECK_DONE["citecheck_id"] + with respx.mock(base_url=BASE) as r: + route = r.post("/citecheck").respond(202, json={"citecheck_id": cid, "status": "queued"}) + r.get(f"/citechecks/{cid}").respond(200, json=CHECK_DONE) + client.citecheck_and_wait(*args, idempotency_key=KEY, **kwargs) + assert _ordered(route.calls.last.request.content) == expected + assert route.calls.last.request.headers["Idempotency-Key"] == KEY From 6b0c23b5f3f1fe19e0522c4f0431c4dd5abb5097 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 13:59:38 +0300 Subject: [PATCH 22/67] feat!: verify_batch and ask.send send an automatic Idempotency-Key One random key per call, reused across that call's own retries, so a retried batch or follow-up question replays the first answer instead of being charged twice. A caller's key wins; idempotency=False sends none. An in-flight 409 is raised as before, never passed with a second key. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 56 ++++++++++++------- tests/test_client.py | 126 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 158 insertions(+), 24 deletions(-) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index d2edf32..194191a 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -63,11 +63,12 @@ * Exponential backoff on transient errors (5xx, 429). 3 retry attempts by default. ``Retry-After`` honored on 429s. * ``Idempotency-Key`` auto-generated per call for ``verify``, - ``verify_and_wait``, ``select``, ``assess`` and ``extract`` (random, reused + ``verify_and_wait``, ``verify_batch``, ``verify_batch_and_wait``, + ``select``, ``assess``, ``extract`` and ``ask.send`` (random, reused across that call's own retries) so a network drop doesn't run the request twice. Customer can override with explicit ``idempotency_key=...`` or opt - out with ``idempotency=False``. ``ask.send`` takes the same argument but - never generates one — see its docstring. + out with ``idempotency=False``. (2.x sent a key on ``verify_batch``, + ``verify_batch_and_wait`` and ``ask.send`` only when you passed one.) * ``X-Lenz-API-Version`` header pinned per SDK release (``API_VERSION``), so the server answers every release in the response shape it was built for. * ``X-Request-ID`` is captured from every response onto the typed error @@ -418,6 +419,7 @@ def send( message: str, language: str = "", idempotency_key: str | None = None, + idempotency: bool = True, ) -> AskReply: """Send a follow-up question on an existing verification. @@ -426,18 +428,21 @@ def send( ``language`` as default — that's the typical case. ``'auto'`` also answers in the language of the claim being discussed. - ``idempotency_key`` (optional): send an ``Idempotency-Key`` so a - retry of *this* question replays the first reply instead of asking — - and paying for — it twice, and without appending the question and a - second answer to the conversation. A retry that arrives while the - first call is still running gets a 409 instead: there is no reply to - replay yet. - - Unlike ``assess``, no key is generated for you, and none is derived - from the message: asking the same thing again is a normal thing to do - here, and each turn also reads the history the previous one wrote, so - an implied key would replay a stale answer. Pass your own key when - your retry means "the same question, once". + ``idempotency`` (default ``True``): send an ``Idempotency-Key`` so the + SDK's own retry after a timeout or network drop replays the first + reply instead of asking, and paying for, the question twice, and + without appending the question and a second answer to the + conversation. The key is random per call and reused across that + call's retries; pin your own with ``idempotency_key=`` (it wins) to + make a retry from another process replay too, or pass + ``idempotency=False`` to send none. A retry that arrives while the + first call is still running gets a 409: there is no reply to replay + yet. (Since 3.0; 2.x sent a key only when you passed one.) + + Never derived from the message: asking the same thing again is a + normal thing to do here, and each turn also reads the history the + previous one wrote, so a key derived from the text would replay a + stale answer. A new call is a new key, so it is asked again. Paid — see ``client.usage()``. @@ -447,8 +452,9 @@ def send( if language: payload["language"] = language headers = {} - if idempotency_key: - headers["Idempotency-Key"] = idempotency_key + key = _call_key(idempotency_key, idempotency) + if key: + headers["Idempotency-Key"] = key body = self._p._request( "POST", f"/ask/{verification_id}", @@ -646,6 +652,7 @@ def verify_batch( visibility: str = "", depth: str = "", idempotency_key: str | None = None, + idempotency: bool = True, ) -> BatchAccepted: """Submit multiple claims in one call. Returns a ``batch_id`` and per-claim ``task_id``s. Each item has its own lifecycle and webhook. @@ -661,6 +668,14 @@ def verify_batch( ``depth`` (optional): batch-wide default, ``'standard'`` or ``'low'`` (shallower check — fewer sources, faster). Each item dict may set its own ``depth`` key to override the batch-wide value. + + ``idempotency`` (default ``True``): send an ``Idempotency-Key`` for + the whole batch, so a retry after a network drop returns the tasks the + first attempt started instead of starting, and paying for, them again. + The key is random per call and reused across this SDK's own retries; + pin your own with ``idempotency_key=`` (it wins), or pass + ``idempotency=False`` to send none. (Since 3.0; 2.x sent a key only + when you passed one.) """ return self._verify_batch( claims=claims, @@ -668,7 +683,7 @@ def verify_batch( language=language, visibility=visibility, depth=depth, - idempotency_key=idempotency_key, + idempotency_key=_call_key(idempotency_key, idempotency), ) def extract( @@ -995,6 +1010,7 @@ def verify_batch_and_wait( idempotency_key: str | None = None, timeout: float = WAIT_TIMEOUT, on_progress: Callable[[str, Progress], None] | None = None, + idempotency: bool = True, ) -> list[BatchItemResult]: """Submit a batch and poll every item to a terminal state. @@ -1010,6 +1026,8 @@ def verify_batch_and_wait( ``timeout`` defaults to ``WAIT_TIMEOUT`` (300s); items still running then come back with ``status="timeout"`` and stay resumable by ``task_id``. + + ``idempotency`` / ``idempotency_key``: as on ``verify_batch``. """ accepted = self._verify_batch( claims=claims, @@ -1017,7 +1035,7 @@ def verify_batch_and_wait( language=language, visibility=visibility, depth=depth, - idempotency_key=idempotency_key, + idempotency_key=_call_key(idempotency_key, idempotency), ) ids = [it.task_id for it in accepted.items if it.task_id] terminal, timed_out, gone = self._poll_to_terminal(ids, timeout, on_progress) diff --git a/tests/test_client.py b/tests/test_client.py index 71983db..70d51c8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -858,6 +858,120 @@ def test_the_key_can_be_pinned_or_disabled(self, client, path, call, body): assert "Idempotency-Key" not in route.calls.last.request.headers +_ASK_REPLY = {"role": "expert", "content": "Because.", "created_at": "2026-05-22T12:00:05Z"} +_BATCH_REPLY = {"batch_id": "b", "items": [{"task_id": "t1", "claim": "A."}]} + +#: The 3.0 additions to the automatic key: batch submit (both helpers) and +#: ask.send. (path, call, success body) +_NEW_AUTO_KEY_CALLS = [ + ("/verify/batch", lambda c, **kw: c.verify_batch(claims=[{"claim": "A."}], **kw), _BATCH_REPLY), + ( + "/verify/batch", + lambda c, **kw: c.verify_batch_and_wait(claims=[{"claim": "A."}], timeout=0, **kw), + {"batch_id": "b", "items": []}, + ), + ("/ask/v1", lambda c, **kw: c.ask.send("v1", message="Why?", **kw), _ASK_REPLY), +] + + +class TestBatchAndAskIdempotency: + """Since 3.0 ``verify_batch``, ``verify_batch_and_wait`` and ``ask.send`` + send a random ``Idempotency-Key`` per call, reused across that call's own + retries: a retried batch or question replays the first answer instead of + being charged twice.""" + + @pytest.mark.parametrize(("path", "call", "body"), _NEW_AUTO_KEY_CALLS) + def test_a_random_key_per_call(self, client, path, call, body): + with respx.mock(base_url=DEFAULT_BASE) as r: + route = r.post(path).respond(200, json=body) + call(client) + call(client) + keys = [c.request.headers["Idempotency-Key"] for c in route.calls] + assert all(re.match(r"^[0-9a-f]{32}$", k) for k in keys), keys + assert keys[0] != keys[1] + + @pytest.mark.parametrize(("path", "call", "body"), _NEW_AUTO_KEY_CALLS) + def test_the_same_key_on_every_retry_attempt(self, client, path, call, body, monkeypatch): + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: None) + with respx.mock(base_url=DEFAULT_BASE) as r: + route = r.post(path) + route.side_effect = [ + httpx.Response(502, json={"detail": "bad gateway"}), + httpx.ReadTimeout("slow"), + httpx.Response(503, json={"detail": "unavailable"}), + httpx.Response(200, json=body), + ] + call(client) + assert len(route.calls) == 4 + assert len({c.request.headers["Idempotency-Key"] for c in route.calls}) == 1 + + @pytest.mark.parametrize(("path", "call", "body"), _NEW_AUTO_KEY_CALLS) + def test_a_retry_after_a_lost_reply_gets_the_replayed_answer(self, client, path, call, body, monkeypatch): + # The first attempt reached the server and its reply was lost; the + # retry carries the same key and body, so the server answers with the + # replay. + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: None) + with respx.mock(base_url=DEFAULT_BASE) as r: + route = r.post(path) + route.side_effect = [httpx.ReadTimeout("reply lost"), httpx.Response(200, json=body)] + out = call(client) + assert len(route.calls) == 2 + first, second = (c.request.headers["Idempotency-Key"] for c in route.calls) + assert first == second + assert route.calls[0].request.content == route.calls[1].request.content + assert out is not None + + @pytest.mark.parametrize(("path", "call", "body"), _NEW_AUTO_KEY_CALLS) + def test_an_in_flight_409_is_never_passed_with_a_second_key(self, client, path, call, body, monkeypatch): + # The retry lands while the first attempt still runs: the server + # answers 409 ``idempotency_conflict``. The SDK raises it, as it does + # for ``verify``; it never mints a new key to get past it, which would + # run (and charge) the request twice. + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: None) + conflict = {"detail": "A request with this key is still running.", "code": "idempotency_conflict"} + with respx.mock(base_url=DEFAULT_BASE) as r: + route = r.post(path) + route.side_effect = [httpx.ReadTimeout("slow"), httpx.Response(409, json=conflict)] + with pytest.raises(LenzError) as ei: + call(client) + assert ei.value.status_code == 409 + # ``code`` keeps its 2.x value (none, on this 409); the body has it. + assert ei.value.body == conflict + assert len(route.calls) == 2 + assert len({c.request.headers["Idempotency-Key"] for c in route.calls}) == 1 + + @pytest.mark.parametrize(("path", "call", "body"), _NEW_AUTO_KEY_CALLS) + def test_a_pinned_key_answers_409_then_the_result(self, client, path, call, body): + # With a key of its own the caller resends the same request after the + # 409 and gets the first call's answer. + conflict = {"detail": "A request with this key is still running.", "code": "idempotency_conflict"} + with respx.mock(base_url=DEFAULT_BASE) as r: + route = r.post(path) + route.side_effect = [httpx.Response(409, json=conflict), httpx.Response(200, json=body)] + with pytest.raises(LenzError): + call(client, idempotency_key="mine-1") + call(client, idempotency_key="mine-1") + assert [c.request.headers["Idempotency-Key"] for c in route.calls] == ["mine-1", "mine-1"] + + @pytest.mark.parametrize(("path", "call", "body"), _NEW_AUTO_KEY_CALLS) + def test_the_caller_key_wins_and_opting_out_sends_none(self, client, path, call, body): + with respx.mock(base_url=DEFAULT_BASE) as r: + route = r.post(path).respond(200, json=body) + call(client, idempotency_key="pinned-1") + assert route.calls.last.request.headers["Idempotency-Key"] == "pinned-1" + call(client, idempotency_key="pinned-2", idempotency=False) + assert route.calls.last.request.headers["Idempotency-Key"] == "pinned-2" + call(client, idempotency=False) + assert "Idempotency-Key" not in route.calls.last.request.headers + + @pytest.mark.parametrize("name", ["verify_batch", "verify_batch_and_wait"]) + def test_the_opt_out_is_named_like_verify(self, name): + param = inspect.signature(getattr(Lenz, name)).parameters["idempotency"] + assert param.default is True + assert param.kind is inspect.Parameter.KEYWORD_ONLY + assert inspect.signature(Lenz(api_key="k").ask.send).parameters["idempotency"].default is True + + class TestWaitDefaults: """The polling helpers wait 300s by default; a timeout still leaves the task resumable.""" @@ -1544,10 +1658,10 @@ def test_send_with_idempotency_key_sets_header(self, client): client.ask.send("vid_1", message="why?", idempotency_key="ask-key-1") assert route.calls.last.request.headers["Idempotency-Key"] == "ask-key-1" - def test_send_sends_no_idempotency_key_by_default(self, client): - # Never auto-generated and never derived from the message: re-asking - # the same question is normal here, so a key the caller did not - # choose would replay an old answer instead of asking again. + def test_send_sends_a_new_random_key_per_call(self, client): + # 3.0: one key per call, so the SDK's own retry replays the first + # reply. Never derived from the message: asking the same question + # again is a new call with a new key, so it is asked again. with respx.mock(base_url=DEFAULT_BASE) as r: route = r.post("/ask/vid_1").respond( 200, @@ -1555,7 +1669,9 @@ def test_send_sends_no_idempotency_key_by_default(self, client): ) client.ask.send("vid_1", message="why?") client.ask.send("vid_1", message="why?") - assert all("Idempotency-Key" not in c.request.headers for c in route.calls) + keys = [c.request.headers["Idempotency-Key"] for c in route.calls] + assert all(re.match(r"^[0-9a-f]{32}$", k) for k in keys), keys + assert keys[0] != keys[1] def test_send_legacy_reply_attr_gone(self): # REGRESSION: pre-1.0.2 `AskReply.reply` always returned `""` From 2e681c87d7b6895d3c2962c271a09450416e949e Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:01:48 +0300 Subject: [PATCH 23/67] feat: LenzNotFoundError, LenzConnectionError, LenzRequestTimeoutError and retryable on every error A 404 raises LenzNotFoundError (a LenzError) whose fix says what to check instead of advising a retry. A network failure raises LenzConnectionError and a transport timeout LenzRequestTimeoutError, both subclasses of the LenzAPIError 2.x raised, with the httpx exception as __cause__. Every error carries retryable, set at construction: the server's value on a failed job, else derived from the status and class; a boolean in the body's failure block wins. ReviewFailedError, ReviewTimeoutError, CitecheckFailedError and CitecheckTimeoutError alias the job errors under the Node names. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/__init__.py | 14 ++++ src/lenz_io/client.py | 13 +-- src/lenz_io/errors.py | 120 +++++++++++++++++++++++++- tests/test_errors.py | 182 +++++++++++++++++++++++++++++++++++++++- tests/test_parity.py | 24 +++++- 5 files changed, 344 insertions(+), 9 deletions(-) diff --git a/src/lenz_io/__init__.py b/src/lenz_io/__init__.py index 23b4d9d..5e87e4a 100644 --- a/src/lenz_io/__init__.py +++ b/src/lenz_io/__init__.py @@ -50,23 +50,30 @@ from .errors import ( MAX_RETRY_AFTER_SLEEP, CitecheckFailed, + CitecheckFailedError, CitecheckTimeout, + CitecheckTimeoutError, LenzAPIError, LenzApiVersionError, LenzAuthError, + LenzConnectionError, LenzError, LenzGoneError, LenzNeedsInputError, + LenzNotFoundError, LenzPipelineError, LenzQuotaExceededError, LenzRateLimitError, + LenzRequestTimeoutError, LenzTimeoutError, LenzUpstreamUnavailableError, LenzValidationError, LenzVerificationNotReadyError, LenzWebhookSignatureError, ReviewFailed, + ReviewFailedError, ReviewTimeout, + ReviewTimeoutError, ) from .models import ( AskHistory, @@ -159,8 +166,10 @@ "Citecheck", "CitecheckEvent", "CitecheckFailed", + "CitecheckFailedError", "CitecheckStarted", "CitecheckTimeout", + "CitecheckTimeoutError", "ClaimLocation", "Coverage", "CoverageReason", @@ -179,12 +188,15 @@ "LenzAPIError", "LenzApiVersionError", "LenzAuthError", + "LenzConnectionError", "LenzError", "LenzGoneError", "LenzNeedsInputError", + "LenzNotFoundError", "LenzPipelineError", "LenzQuotaExceededError", "LenzRateLimitError", + "LenzRequestTimeoutError", "LenzTimeoutError", "LenzUpstreamUnavailableError", "LenzValidationError", @@ -202,12 +214,14 @@ "ReviewClaim", "ReviewEvent", "ReviewFailed", + "ReviewFailedError", "ReviewFailure", "ReviewFull", "ReviewIssue", "ReviewIssues", "ReviewStarted", "ReviewTimeout", + "ReviewTimeoutError", "SimilarVerification", "Source", "SuggestedEdit", diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 194191a..4af3deb 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -95,11 +95,14 @@ CitecheckTimeout, LenzAPIError, LenzApiVersionError, + LenzAuthError, + LenzConnectionError, LenzError, LenzGoneError, LenzNeedsInputError, LenzPipelineError, LenzRateLimitError, + LenzRequestTimeoutError, LenzTimeoutError, ReviewFailed, ReviewTimeout, @@ -344,8 +347,8 @@ def get_certificate(self, verification_id: str) -> Certificate: certificate and their own cap, so this returns YOUR certificate over this analysis and never another customer's. - Raises ``LenzError`` with status 404 when this verification carries no - certificate for your account — which is also what an uncovered verdict + Raises :class:`LenzNotFoundError` (a ``LenzError``, status 404) when + this verification carries no certificate for your account — which is also what an uncovered verdict returns, so check ``verification.coverage.status`` first rather than using a 404 here to mean "not covered". @@ -1790,8 +1793,6 @@ def _request( # only (the review wait polls with 0 and does its own pacing). retries = self._max_retries if max_retries is None else max_retries if auth_required and not self._api_key: - from .errors import LenzAuthError - raise LenzAuthError( message="API key required", cause="This method requires authentication; no API key was provided.", @@ -1829,7 +1830,9 @@ def _request( except (httpx.TimeoutException, httpx.NetworkError) as exc: last_exc = exc if attempt >= retries: - raise LenzAPIError( + # Subclasses of LenzAPIError, which is what 2.x raised. + cls = LenzRequestTimeoutError if isinstance(exc, httpx.TimeoutException) else LenzConnectionError + raise cls( message=f"{method} {path} failed after {attempt + 1} attempts: {exc}", cause=str(exc), fix="Check your network connection; verify base_url is reachable.", diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index b383cb4..7a1586f 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -49,8 +49,18 @@ class LenzError(Exception): ``"no_credits"``. Present on 402, 403 and 429; ``""`` when the server sent none. Branch on this rather than on message text. * ``body`` — parsed JSON response body if available + * ``retryable`` — whether sending the same request again can succeed: + ``True`` for a network failure, a transport timeout, a 429 and a 5xx; + ``False`` for any other 4xx, a version error and the client-side + errors a resend cannot fix; ``None`` when the SDK cannot say. A failed + verification, review or citation check carries the server's value + (``None`` when it sent none), and a boolean ``retryable`` in the + response's ``failure`` block always wins. Since 3.0; set on every + instance at construction, and a ``retryable=`` passed in wins. """ + retryable: bool | None + def __init__( self, *, @@ -73,12 +83,23 @@ def __init__( self.status_code = status_code self.code = code self.body = body + self.retryable = self._derived_retryable() # Per-subclass enrichment (retry_after, task_id, etc.). Set on the # instance so they're accessible as ``exc.task_id`` regardless of # which subclass raised. for k, v in extra.items(): setattr(self, k, v) + def _derived_retryable(self) -> bool | None: + """``retryable`` when nothing more specific says (see the class + docstring). A class with its own rule overrides this.""" + status = self.status_code + if status == 429 or 500 <= status < 600: + return True + if 400 <= status < 500: + return False + return None + def __str__(self) -> str: # pragma: no cover - trivial lines = [self.message or self.__class__.__name__] if self.cause: @@ -103,6 +124,10 @@ class LenzAuthError(LenzError): and "top up your account" are different actions. """ + def _derived_retryable(self) -> bool | None: + # Also the client-side "API key required" (status 0). + return False + class LenzQuotaExceededError(LenzError): """402 — you're out of balance, or your plan doesn't cover this call. @@ -226,16 +251,48 @@ class LenzRateLimitError(LenzError): #: know a paid plan raises it. upgrade_url: str = "" + def _derived_retryable(self) -> bool | None: + return True + class LenzAPIError(LenzError): """500 / 502 / 503 / 504 / catch-all for unexpected server errors. ``retry_after`` is the wait the response stated (``Retry-After``), or ``None`` when it stated none. + + Network failures are :class:`LenzConnectionError`, a subclass. """ retry_after: int | None = None + def _derived_retryable(self) -> bool | None: + return True + + +class LenzConnectionError(LenzAPIError): + """The request never got an answer: the connection failed or broke + (DNS, refused, reset, TLS), after the SDK's own retries. + + A subclass of :class:`LenzAPIError`, which is what 2.x raised, so an + existing ``except LenzAPIError`` keeps catching it. ``__cause__`` is the + underlying ``httpx`` exception and ``status_code`` is 0. ``retryable`` is + ``True``: the same request can succeed once the network is back (calls + that charge send an ``Idempotency-Key``, so a resend replays rather than + runs twice). + """ + + +class LenzRequestTimeoutError(LenzConnectionError): + """One HTTP request got no answer within its timeout (``Lenz(timeout=)`` + or the call's ``timeout=``), after the SDK's own retries. + + Not :class:`LenzTimeoutError`, which is a ``*_and_wait`` helper reaching + its own deadline while the job keeps running. A subclass of + :class:`LenzConnectionError` and so of :class:`LenzAPIError`, which is + what 2.x raised. + """ + class LenzUpstreamUnavailableError(LenzAPIError): """503 with ``code`` ``upstream_unavailable`` or ``capacity``. @@ -259,10 +316,16 @@ class LenzTimeoutError(LenzError): """``verify_and_wait`` exceeded the configured timeout. ``task_id`` is set so callers can resume via ``client.get_status(task_id)``. + The job keeps running server-side: read it later rather than resubmit, so + ``retryable`` is ``False``. A single HTTP request that timed out is + :class:`LenzRequestTimeoutError` instead. """ task_id: str = "" + def _derived_retryable(self) -> bool | None: + return False + class LenzNeedsInputError(LenzError): """``verify_and_wait`` paused because the pipeline needs caller input. @@ -278,6 +341,10 @@ class LenzNeedsInputError(LenzError): hint: str = "" payload: dict[str, Any] = {} # noqa: RUF012 — overridden per-instance + def _derived_retryable(self) -> bool | None: + # A resend pauses the same way: resolve it with ``select``. + return False + class LenzPipelineError(LenzError): """A verification run ended in a terminal ``failed`` state. @@ -301,6 +368,10 @@ class LenzPipelineError(LenzError): retryable: bool | None = None hint: str = "" + def _derived_retryable(self) -> bool | None: + # The server's value or nothing: never guessed from a status. + return None + class LenzVerificationNotReadyError(LenzError): """409 — ``verifications.get`` was handed the ``task_id`` of a run that is @@ -321,6 +392,17 @@ class LenzVerificationNotReadyError(LenzError): hint: str = "" +class LenzNotFoundError(LenzError): + """404 — nothing was found under the id (or path) the request names, for + the API key it was sent with. + + Check the id and the key: resending the same request will not find it + (``retryable`` is ``False``). A subclass of :class:`LenzError`, which is + what 2.x raised for a 404. An id whose verification its account's + retention period removed answers 410 instead (:class:`LenzGoneError`). + """ + + class LenzGoneError(LenzError): """410 — the verification existed, and its account's retention period has since removed it. @@ -332,8 +414,8 @@ class LenzGoneError(LenzError): Raised by ``verifications.get``, ``get_status`` on a completed task, ``verifications.related``, ``ask.send`` and ``ask.history``. ``wait`` raises it at once instead - of polling to the deadline. A 404 stays a plain :class:`LenzError`: only - an id you could read before answers 410. + of polling to the deadline. A 404 is :class:`LenzNotFoundError`: only an + id you could read before answers 410. """ purged_at: str | None = None @@ -401,6 +483,9 @@ class LenzWebhookSignatureError(LenzError): ``X-Lenz-Signature`` header, replay window exceeded, malformed body. """ + def _derived_retryable(self) -> bool | None: + return False + class LenzApiVersionError(LenzError): """A response named an API version this SDK does not read. @@ -424,6 +509,18 @@ def __init__(self, *, api_version: str = "", **kwargs: Any) -> None: super().__init__(api_version=api_version, **kwargs) self.api_version = api_version + def _derived_retryable(self) -> bool | None: + # The same request answers in the same version again. + return False + + +#: The job errors under the names the other error classes follow (``...Error``), +#: the names the Node SDK uses. The same classes: catch either name. +ReviewFailedError = ReviewFailed +ReviewTimeoutError = ReviewTimeout +CitecheckFailedError = CitecheckFailed +CitecheckTimeoutError = CitecheckTimeout + # ── Mapping table ──────────────────────────────────────────────────────── # @@ -717,6 +814,8 @@ def map_response_to_error( if status_code in _STATUS_MAP: cls, default_msg, doc_url = _STATUS_MAP[status_code] + elif status_code == 404: + cls, default_msg, doc_url = LenzNotFoundError, f"HTTP {status_code}", f"{_DOCS_BASE}/errors" elif status_code == 410 and code in _GONE_410_CODES: cls, default_msg, doc_url = _GONE_410_CODES[code] elif status_code == 409 and code in _VERIFICATION_409_CODES: @@ -872,6 +971,15 @@ def _read(key: str, block_key: str) -> Any: else: err.retry_after = 0 + # A boolean ``retryable`` in the body's failure block is the server's own + # word and wins over the status. (A failed verification's 409 read it + # above, with the flat field first.) + if not isinstance(err, LenzPipelineError): + for block in (raw.get("failure"), parsed.get("failure")): + if isinstance(block, dict) and isinstance(block.get("retryable"), bool): + err.retryable = block["retryable"] + break + return err @@ -905,6 +1013,7 @@ def _fix_hint_for(status_code: int) -> str: 401: "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", 403: "This key doesn't have access to that resource.", 402: "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", + 404: "Check the id the request names and the API key it was sent with: retrying will not find it.", 422: "Check the request body against the OpenAPI spec.", 429: "Wait Retry-After seconds and retry.", }.get(status_code, "Retry; if the error persists, file an issue with the Request ID.") @@ -915,22 +1024,29 @@ def _fix_hint_for(status_code: int) -> str: "NO_RETRY_429_CODES", "UPSTREAM_503_CODES", "CitecheckFailed", + "CitecheckFailedError", "CitecheckTimeout", + "CitecheckTimeoutError", "LenzAPIError", "LenzApiVersionError", "LenzAuthError", + "LenzConnectionError", "LenzError", "LenzGoneError", "LenzNeedsInputError", + "LenzNotFoundError", "LenzPipelineError", "LenzQuotaExceededError", "LenzRateLimitError", + "LenzRequestTimeoutError", "LenzTimeoutError", "LenzUpstreamUnavailableError", "LenzValidationError", "LenzVerificationNotReadyError", "LenzWebhookSignatureError", "ReviewFailed", + "ReviewFailedError", "ReviewTimeout", + "ReviewTimeoutError", "map_response_to_error", ] diff --git a/tests/test_errors.py b/tests/test_errors.py index 6dfed18..459a565 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -336,7 +336,7 @@ def test_410_without_purged_code_stays_plain(self): assert e.status_code == 410 def test_gone_is_not_a_not_found(self): - # A 404 stays a plain LenzError: only 410 is gone. + # A 404 is a not-found error, never a gone one: only 410 is gone. e = map_response_to_error(404, _body({"detail": "Not found."}), {}) assert not isinstance(e, LenzGoneError) @@ -475,3 +475,183 @@ def test_errors_all_lists_every_public_error_class_and_constant(): assert {"LenzGoneError", "LenzUpstreamUnavailableError", "UPSTREAM_503_CODES"} <= set(errors.__all__) for name in errors.__all__: assert hasattr(errors, name), name + + +# ── 3.0: not found, connection failures, ``retryable`` ───────────────────── + +NOT_FOUND_FIX = "Check the id the request names and the API key it was sent with: retrying will not find it." + + +class TestNotFound: + def test_404_is_its_own_error_and_still_a_lenz_error(self): + from lenz_io import LenzNotFoundError + + e = map_response_to_error(404, _body({"detail": "Not found."}), {"X-Request-ID": "rq9"}) + assert type(e) is LenzNotFoundError + assert isinstance(e, LenzError) + assert not isinstance(e, (LenzGoneError, LenzAPIError)) + assert e.status_code == 404 + assert e.request_id == "rq9" + + def test_404_keeps_its_message_and_says_what_to_check(self): + e = map_response_to_error(404, _body({"detail": "Not found."}), {}) + assert e.message == "Not found." + assert e.cause == "Not found." + assert e.doc_url == "https://lenz.io/docs/errors" + assert e.fix == NOT_FOUND_FIX + assert ( + str(e) + == f"Not found.\n Cause: Not found.\n Fix: {NOT_FOUND_FIX}\n Docs: https://lenz.io/docs/errors" + ) + + def test_404_without_a_body_keeps_the_2x_message(self): + e = map_response_to_error(404, b"", {}) + assert e.message == "HTTP 404" + + def test_other_errors_keep_their_text(self): + e = map_response_to_error(500, _body({"detail": "boom"}), {}) + assert e.fix == "Retry; if the error persists, file an issue with the Request ID." + assert repr(e) == "LenzAPIError('boom')" + + +class TestConnectionErrors: + def _fail(self, exc: Exception): + import respx + + from lenz_io import Lenz + + with respx.mock(base_url="https://lenz.io/api/v1") as r: + r.get("/me/usage").mock(side_effect=exc) + with Lenz(api_key="lenz_test", max_retries=0) as client: + try: + client.usage() + except LenzError as caught: + return caught + raise AssertionError("no error raised") # pragma: no cover + + def test_a_network_failure_is_a_connection_error_and_still_an_api_error(self): + import httpx + + from lenz_io import LenzConnectionError, LenzRequestTimeoutError, LenzTimeoutError + + cause = httpx.ConnectError("connection refused") + e = self._fail(cause) + assert type(e) is LenzConnectionError + assert isinstance(e, LenzAPIError) + assert not isinstance(e, (LenzRequestTimeoutError, LenzTimeoutError)) + assert e.__cause__ is cause + assert e.message == "GET /me/usage failed after 1 attempts: connection refused" + assert e.fix == "Check your network connection; verify base_url is reachable." + assert e.retryable is True + + def test_a_transport_timeout_is_a_request_timeout_not_a_job_timeout(self): + import httpx + + from lenz_io import LenzConnectionError, LenzRequestTimeoutError, LenzTimeoutError + + e = self._fail(httpx.ReadTimeout("timed out")) + assert type(e) is LenzRequestTimeoutError + assert isinstance(e, LenzConnectionError) + assert isinstance(e, LenzAPIError) + assert not isinstance(e, LenzTimeoutError) + assert e.message == "GET /me/usage failed after 1 attempts: timed out" + assert e.retryable is True + + +class TestRetryable: + @pytest.mark.parametrize( + ("status", "body", "expected"), + [ + (400, {}, False), + (401, {}, False), + (402, {"code": "no_credits"}, False), + (403, {}, False), + (404, {}, False), + (409, {"code": "idempotency_conflict"}, False), + (410, {"code": "purged"}, False), + (422, {}, False), + (429, {}, True), + (429, {"code": "review_in_flight"}, True), + (500, {}, True), + (502, {}, True), + (503, {}, True), + (503, {"code": "upstream_unavailable"}, True), + (503, {"code": "capacity"}, True), + (409, {"code": "verification_not_ready", "status": "processing"}, False), + ], + ) + def test_derived_from_the_response(self, status, body, expected): + assert map_response_to_error(status, _body(body), {}).retryable is expected + + @pytest.mark.parametrize(("status", "flag"), [(503, False), (400, True), (500, False), (422, True)]) + def test_a_boolean_in_the_failure_block_wins(self, status, flag): + e = map_response_to_error(status, _body({"detail": "x", "failure": {"retryable": flag}}), {}) + assert e.retryable is flag + + def test_a_non_boolean_in_the_failure_block_is_ignored(self): + e = map_response_to_error(503, _body({"failure": {"retryable": "yes"}}), {}) + assert e.retryable is True + + def test_a_failed_verification_keeps_the_servers_value_unknown_included(self): + failed = {"detail": "Verification failed.", "code": "verification_failed", "task_id": _TASK_ID} + assert map_response_to_error(409, _body(failed), {}).retryable is None + failed["retryable"] = True + assert map_response_to_error(409, _body(failed), {}).retryable is True + + def test_a_version_error_is_not_retryable(self): + from lenz_io import LenzApiVersionError + + assert LenzApiVersionError(api_version="2026-05-13", status_code=200).retryable is False + + def test_client_side_errors(self): + from lenz_io import ( + LenzNeedsInputError, + LenzTimeoutError, + LenzWebhookSignatureError, + ReviewFailed, + ReviewTimeout, + ) + + assert LenzError().retryable is None + assert LenzPipelineError().retryable is None + assert ReviewFailed().retryable is None + assert LenzAuthError(message="API key required").retryable is False + assert LenzTimeoutError().retryable is False + assert ReviewTimeout().retryable is False + assert LenzNeedsInputError().retryable is False + assert LenzWebhookSignatureError().retryable is False + assert LenzAPIError().retryable is True + assert LenzRateLimitError().retryable is True + + def test_a_value_passed_in_wins(self): + assert LenzPipelineError(retryable=True).retryable is True + assert LenzAPIError(retryable=False).retryable is False + assert LenzError(status_code=503, retryable=None).retryable is None + + def test_it_is_an_instance_field_not_a_property(self): + e = map_response_to_error(500, b"{}", {}) + assert "retryable" in vars(e) + e.retryable = False + assert e.retryable is False + + +def test_the_job_error_aliases_are_the_same_classes(): + import lenz_io + from lenz_io import errors + + for alias, original in ( + ("ReviewFailedError", "ReviewFailed"), + ("ReviewTimeoutError", "ReviewTimeout"), + ("CitecheckFailedError", "CitecheckFailed"), + ("CitecheckTimeoutError", "CitecheckTimeout"), + ): + assert getattr(lenz_io, alias) is getattr(lenz_io, original) + assert alias in errors.__all__ and alias in lenz_io.__all__ + + +def test_the_new_classes_are_exported(): + import lenz_io + + for name in ("LenzNotFoundError", "LenzConnectionError", "LenzRequestTimeoutError"): + assert name in lenz_io.__all__ + assert issubclass(getattr(lenz_io, name), LenzError) diff --git a/tests/test_parity.py b/tests/test_parity.py index 4f603ed..efce38b 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -113,6 +113,26 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, } +#: The fix line 3.0 gives a 404 (CHANGELOG "Changed"): 2.x said to retry. +_NOT_FOUND_FIX = "Check the id the request names and the API key it was sent with: retrying will not find it." + + +def _intended(path: str, old: Any, new: Any, legacy: dict[str, Any]) -> str | None: + """Why ``path`` changed on purpose in 3.0 (each listed in the CHANGELOG), + or ``None``.""" + head, _, rest = path.partition(".") + if head in ("error", "wait") and rest == "retryable" and old is MISSING and new in (True, False, None): + return "3.0 sets `retryable` on every error" + if legacy["status"] == 404 and head == "error": + if rest == "type" and (old, new) == ("LenzError", "LenzNotFoundError"): + return "3.0 raises LenzNotFoundError (a LenzError) for a 404" + if rest in ("fix", "payload_json.error.fix") and new == _NOT_FOUND_FIX: + return "3.0 says what to check on a 404 instead of advising a retry" + if rest == "friendly_text" and isinstance(new, str) and new.endswith(_NOT_FOUND_FIX): + return "the same fix line, printed by the CLI" + return None + + def _expected(name: str) -> dict[str, Any]: return json.loads((EXPECTED / name).read_text()) @@ -189,7 +209,9 @@ def test_newer_shape_reads_the_same(name: str) -> None: got = _canonical_view(name, expected) legacy, canonical = load("legacy", name), load("canonical", name) gaps = [ - (path, old, new) for path, old, new in _diff(expected, got) if _allowed(name, path, legacy, canonical) is None + (path, old, new) + for path, old, new in _diff(expected, got) + if _allowed(name, path, legacy, canonical) is None and _intended(path, old, new, legacy) is None ] assert not gaps, "\n".join(f"{path}: {old!r} -> {new!r}" for path, old, new in gaps) From de166ef146f8d6dd6f52ceb427feb5e94b8739e5 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:05:50 +0300 Subject: [PATCH 24/67] fix!: waits stop at once on a 401, 403 or 404 instead of polling to a timeout wait, verify_and_wait and verify_batch_and_wait polled through every error until their deadline, so a revoked key or a wrong task id surfaced minutes later as a LenzTimeoutError. They now end with the error (a batch item is failed, the others continue); a 5xx, a 429 or a network failure is still polled again, after any wait it stated. Each poll is one request bounded by what is left of the deadline. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 91 ++++++++++++------- tests/test_wait_stops.py | 192 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 32 deletions(-) create mode 100644 tests/test_wait_stops.py diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 4af3deb..a2cf598 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -100,6 +100,7 @@ LenzError, LenzGoneError, LenzNeedsInputError, + LenzNotFoundError, LenzPipelineError, LenzRateLimitError, LenzRequestTimeoutError, @@ -985,13 +986,19 @@ def wait( removed the verification, and ``LenzTimeoutError`` if ``timeout`` (default ``WAIT_TIMEOUT``, 300s) elapses (the task may still finish server-side — resume via ``get_status``). + + A poll answered 401 / 403 (``LenzAuthError``) or 404 + (``LenzNotFoundError``) ends the wait at once with that error: no + later poll would answer otherwise. A 5xx, a 429 or a network failure + is polled again on the next round. (Since 3.0; 2.x polled through + every error until the timeout.) """ task_id = task if isinstance(task, str) else task.task_id if not task_id: raise ValueError("wait() requires a non-empty task_id (got an empty TaskAccepted.task_id).") - terminal, timed_out, gone = self._poll_to_terminal([task_id], timeout, on_progress) - if task_id in gone: - raise gone[task_id] + terminal, timed_out, stopped = self._poll_to_terminal([task_id], timeout, on_progress) + if task_id in stopped: + raise stopped[task_id] if task_id in timed_out: raise LenzTimeoutError( message=f"wait timed out after {timeout}s", @@ -1021,7 +1028,9 @@ def verify_batch_and_wait( order**. Never raises on a per-item outcome — a claim that fails, pauses for input, or times out becomes a ``BatchItemResult`` with the matching ``status`` rather than an exception. (Transport/auth errors on the - initial submit still raise.) + initial submit still raise.) An item whose poll is answered 401 / 403 / + 404 or 410 is ``failed`` at once, with no ``status_detail``; the other + items keep being polled. ``on_progress(task_id, progress)`` fires per still-running item per round; the ``task_id`` is what tells you which claim moved. @@ -1041,14 +1050,15 @@ def verify_batch_and_wait( idempotency_key=_call_key(idempotency_key, idempotency), ) ids = [it.task_id for it in accepted.items if it.task_id] - terminal, timed_out, gone = self._poll_to_terminal(ids, timeout, on_progress) + terminal, timed_out, stopped = self._poll_to_terminal(ids, timeout, on_progress) results: list[BatchItemResult] = [] for it in accepted.items: # preserve input order status = terminal.get(it.task_id) - if it.task_id in gone: - # Removed by the account's retention period (410): terminal, - # with no status to carry. + if it.task_id in stopped: + # An error no later poll could change: removed by the + # account's retention period (410), a refused key (401 / 403) + # or an unknown task (404). Terminal, with no status to carry. results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="failed")) elif not it.task_id or it.task_id in timed_out or status is None: results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="timeout")) @@ -1472,29 +1482,32 @@ def _poll_to_terminal( task_ids: list[str], timeout: float, on_progress: Callable[[str, Progress], None] | None = None, - ) -> tuple[dict[str, TaskStatus], set[str], dict[str, LenzGoneError]]: + ) -> tuple[dict[str, TaskStatus], set[str], dict[str, LenzError]]: """Round-robin poll ``task_ids`` until each reaches a terminal state (completed / needs_input / failed) or the deadline elapses. - Returns ``(terminal_by_id, timed_out_ids, gone_by_id)``. A timed-out task has no - ``TaskStatus`` — ``"timeout"`` is a client-side concept, never a wire - status — so it lands in the second set, not the dict. + Returns ``(terminal_by_id, timed_out_ids, stopped_by_id)``. A timed-out + task has no ``TaskStatus`` — ``"timeout"`` is a client-side concept, + never a wire status — so it lands in the second set, not the dict. Ordering preserves the legacy ``verify_and_wait`` behavior: each round polls every still-pending id once *before* the deadline check, so after sleeping the remaining time we always poll once more and can succeed - just past the nominal deadline. The timeout is therefore approximate: - the final round finishes polling every pending id (bounded by the - per-request timeout + retries) before any still-pending ids are marked - timed out. Backoff reuses the existing 2/4/8/8…s sequence; the 10s cap + just past the nominal deadline. Each poll is ONE request whose timeout + is what is left of the deadline (at least 1s, at most the client's + timeout): the client's own retry ladder inside a poll could run + minutes past it, so a failed poll is retried on the next round + instead. Backoff reuses the existing 2/4/8/8…s sequence; the 10s cap is currently unreachable and kept only to preserve identical timing. - A per-id poll that raises ``LenzError`` (e.g. a transport blip that - outlived ``_request``'s own retries) does not abort the other ids: that - id stays pending and is retried next round. A persistent error - therefore surfaces as a timeout once the deadline passes. The one - exception is ``LenzGoneError`` (410): it is terminal, so that id stops - being polled and its error lands in ``gone_by_id``. + A per-id poll that fails with a 5xx, a 429, a network failure or any + other error a later poll can change does not abort the other ids: that + id stays pending and is polled next round (after the wait the server + stated, if it stated one), so a persistent one surfaces as a timeout + once the deadline passes. An error no later poll can change stops that + id at once and lands in ``stopped_by_id``: a 410 (``LenzGoneError``, + retention removed it), a 401 / 403 (``LenzAuthError``) and a 404 + (``LenzNotFoundError``). A version error is raised for the whole wait. ``on_progress(task_id, progress)`` fires once per still-running poll. It takes the id as well as the object because this loop round-robins a @@ -1508,7 +1521,7 @@ def _poll_to_terminal( ladder when it is present and sane; garbage falls back to the ladder. """ pending = list(task_ids) - gone: dict[str, LenzGoneError] = {} + stopped: dict[str, LenzError] = {} terminal: dict[str, TaskStatus] = {} timed_out: set[str] = set() deadline = time.monotonic() + timeout @@ -1516,21 +1529,33 @@ def _poll_to_terminal( while pending: still_pending: list[str] = [] server_hint: float | None = None + stated_wait: float | None = None for task_id in pending: + remaining = deadline - time.monotonic() try: - status = self._get_status(task_id) - except LenzGoneError as exc: - # Terminal: retention removed it, and no later poll will - # say otherwise. The API never answers 410 for a task - # that is still running. - gone[task_id] = exc + body = self._request( + "GET", + f"/verify/status/{task_id}", + max_retries=0, + timeout=max(1.0, min(remaining, self._timeout)), + ) + status = TaskStatus.model_validate(body) + except (LenzGoneError, LenzAuthError, LenzNotFoundError) as exc: + # Terminal: no later poll will say otherwise. (The API + # never answers 410 for a task that is still running.) + stopped[task_id] = exc continue except LenzApiVersionError: # Not a failed poll: every later poll would answer the same. raise - except LenzError: + except LenzError as exc: # Don't let one id's poll failure abort the rest — retry it - # next round (bounded by the deadline below). + # next round (bounded by the deadline below), after the + # wait it stated, capped like the retry ladder caps it. + wait = getattr(exc, "retry_after", None) + if isinstance(wait, int) and not isinstance(wait, bool) and wait > 0: + wait_s = float(min(wait, MAX_RETRY_AFTER_SLEEP)) + stated_wait = wait_s if stated_wait is None else max(stated_wait, wait_s) still_pending.append(task_id) continue if status.status in ("completed", "needs_input", "failed"): @@ -1559,10 +1584,12 @@ def _poll_to_terminal( sleep_for = server_hint else: sleep_for = min(POLL_BACKOFF[min(backoff_idx, len(POLL_BACKOFF) - 1)], POLL_BACKOFF_CAP) + if stated_wait is not None: + sleep_for = max(sleep_for, stated_wait) sleep_for = min(sleep_for, remaining) time.sleep(sleep_for) backoff_idx += 1 - return terminal, timed_out, gone + return terminal, timed_out, stopped def _verification_from_terminal(self, status: TaskStatus, task_id: str) -> Verification: """Map a terminal ``TaskStatus`` to a ``Verification`` or raise the diff --git a/tests/test_wait_stops.py b/tests/test_wait_stops.py new file mode 100644 index 0000000..ba9d896 --- /dev/null +++ b/tests/test_wait_stops.py @@ -0,0 +1,192 @@ +"""The wait helpers stop at once on an error no later poll can change. + +Before 3.0 ``wait`` (and so ``verify_and_wait`` and ``verify_batch_and_wait``) +retried every failed poll until its deadline, so a revoked key or a wrong +task id surfaced minutes later as a misleading ``LenzTimeoutError``. A 401, +403 or 404 (and a version error) now ends the wait with that error; a 5xx, a +429 or a network failure is still retried on the next round. Each poll is one +HTTP request bounded by what is left of the deadline. +""" + +from __future__ import annotations + +import httpx +import pytest +import respx + +from lenz_io import ( + Lenz, + LenzAuthError, + LenzError, + LenzNotFoundError, + LenzTimeoutError, +) + +BASE = "https://lenz.io/api/v1" +_DONE = {"status": "completed", "task_id": "t", "result": {"verification_id": "v1", "claim": "A."}} +_RUNNING = {"status": "processing", "task_id": "t", "progress": {"step": "research"}} + + +@pytest.fixture() +def slept(monkeypatch: pytest.MonkeyPatch) -> list[float]: + calls: list[float] = [] + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: calls.append(s)) + return calls + + +@pytest.mark.parametrize( + ("status", "body", "cls"), + [ + (401, {"detail": "Invalid API key."}, LenzAuthError), + (403, {"detail": "Forbidden."}, LenzAuthError), + (404, {"detail": "Not found."}, LenzNotFoundError), + ], +) +class TestPermanentErrorsStopTheWait: + def test_wait_raises_it_at_once(self, client: Lenz, slept: list[float], status, body, cls) -> None: + with respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t") + poll.side_effect = [httpx.Response(200, json=_RUNNING), httpx.Response(status, json=body)] + with pytest.raises(cls) as ei: + client.wait("t", timeout=300) + assert poll.call_count == 2 + assert len(slept) == 1, "no sleep after the permanent error" + assert not isinstance(ei.value, LenzTimeoutError) + assert ei.value.status_code == status + + def test_verify_and_wait_raises_it(self, client: Lenz, slept: list[float], status, body, cls) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/verify").respond(200, json={"task_id": "t"}) + poll = r.get("/verify/status/t").respond(status, json=body) + with pytest.raises(cls): + client.verify_and_wait("A.", timeout=300) + assert poll.call_count == 1 + assert slept == [] + + def test_a_batch_item_fails_and_the_others_continue( + self, client: Lenz, slept: list[float], status, body, cls + ) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/verify/batch").respond( + 200, json={"batch_id": "b", "items": [{"task_id": "a", "claim": "A."}, {"task_id": "t", "claim": "B."}]} + ) + bad = r.get("/verify/status/a").respond(status, json=body) + good = r.get("/verify/status/t") + good.side_effect = [httpx.Response(200, json=_RUNNING), httpx.Response(200, json=_DONE)] + results = client.verify_batch_and_wait(claims=[{"claim": "A."}, {"claim": "B."}], timeout=300) + assert [x.status for x in results] == ["failed", "completed"] + assert results[0].status_detail is None + assert results[0].verification is None + assert bad.call_count == 1 + assert good.call_count == 2 + + +class TestTransientErrorsAreRetried: + @pytest.mark.parametrize( + "failure", + [ + httpx.Response(500, json={"detail": "boom"}), + httpx.Response(502, text="bad gateway"), + httpx.Response(503, json={"detail": "unavailable"}), + httpx.Response(429, json={"detail": "slow down"}), + httpx.ConnectError("refused"), + httpx.ReadTimeout("slow"), + ], + ) + def test_the_next_round_polls_again(self, client: Lenz, slept: list[float], failure) -> None: + with respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t") + poll.side_effect = [failure, httpx.Response(200, json=_DONE)] + out = client.wait("t", timeout=300) + assert out.verification_id == "v1" + assert poll.call_count == 2 + + def test_a_stated_wait_paces_the_next_poll(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t") + poll.side_effect = [ + httpx.Response(429, json={"detail": "slow down"}, headers={"Retry-After": "7"}), + httpx.Response(200, json=_DONE), + ] + client.wait("t", timeout=300) + assert slept == [7.0] + + def test_a_persistent_5xx_still_ends_in_a_timeout(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + r.get("/verify/status/t").respond(500, json={"detail": "boom"}) + with pytest.raises(LenzTimeoutError): + client.wait("t", timeout=0) + + def test_other_4xx_keep_the_2x_behaviour(self, client: Lenz, slept: list[float]) -> None: + # Only 401, 403, 404 (and 410, a version error) end a wait early. + with respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t") + poll.side_effect = [httpx.Response(409, json={"detail": "busy"}), httpx.Response(200, json=_DONE)] + client.wait("t", timeout=300) + assert poll.call_count == 2 + + +class TestEachPollIsBounded: + def test_one_request_per_poll(self, client: Lenz, slept: list[float]) -> None: + # A failed poll is retried on the next round, never inside the poll by + # the client's own retry ladder (which could run past the deadline). + with respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t") + poll.side_effect = [httpx.Response(503, json={"detail": "x"}), httpx.Response(200, json=_DONE)] + client.wait("t", timeout=300) + assert poll.call_count == 2 + assert len(slept) == 1 + + def test_the_request_timeout_never_runs_past_the_deadline(self, slept: list[float]) -> None: + with Lenz(api_key="lenz_test", timeout=60.0) as client, respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t") + poll.side_effect = [httpx.Response(200, json=_RUNNING), httpx.Response(200, json=_DONE)] + client.wait("t", timeout=5) + timeouts = [c.request.extensions["timeout"]["read"] for c in poll.calls] + assert all(t <= 5 for t in timeouts), timeouts + + def test_the_final_poll_still_runs_with_a_floor(self, slept: list[float]) -> None: + # Past the deadline the loop still polls once more, with at least 1s. + with Lenz(api_key="lenz_test") as client, respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_RUNNING) + with pytest.raises(LenzTimeoutError): + client.wait("t", timeout=0) + assert poll.calls.last.request.extensions["timeout"]["read"] == 1.0 + + def test_the_client_timeout_bounds_a_long_wait(self, slept: list[float]) -> None: + with Lenz(api_key="lenz_test", timeout=20.0) as client, respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_DONE) + client.wait("t", timeout=300) + assert poll.calls.last.request.extensions["timeout"]["read"] == 20.0 + + +class TestJobWaits: + """``review_and_wait`` and ``citecheck_and_wait`` already stopped on a + 401, 403 or 404 and retried the rest; pinned here beside the others.""" + + @pytest.mark.parametrize(("status", "cls"), [(401, LenzAuthError), (403, LenzAuthError), (404, LenzNotFoundError)]) + def test_review_wait_raises_at_once(self, client: Lenz, slept: list[float], status, cls) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/review").respond(202, json={"review_id": "r1", "status": "queued"}) + poll = r.get("/reviews/r1").respond(status, json={"detail": "x", "code": "not_found"}) + with pytest.raises(cls): + client.review_and_wait("Draft.", timeout=300) + assert poll.call_count == 1 + + @pytest.mark.parametrize(("status", "cls"), [(401, LenzAuthError), (404, LenzNotFoundError)]) + def test_citecheck_wait_raises_at_once(self, client: Lenz, slept: list[float], status, cls) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/citecheck").respond(202, json={"citecheck_id": "c1", "status": "queued"}) + poll = r.get("/citechecks/c1").respond(status, json={"detail": "x"}) + with pytest.raises(cls): + client.citecheck_and_wait("Draft.", timeout=300) + assert poll.call_count == 1 + + def test_a_connection_error_mid_wait_is_retried(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/citecheck").respond(202, json={"citecheck_id": "c1", "status": "queued"}) + poll = r.get("/citechecks/c1") + poll.side_effect = [httpx.ConnectError("refused"), httpx.Response(404, json={"detail": "x"})] + with pytest.raises(LenzError): + client.citecheck_and_wait("Draft.", timeout=300) + assert poll.call_count == 2 From 7a2bfbddc7f240cd47e35646caf9b8d7e3609047 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:06:33 +0300 Subject: [PATCH 25/67] feat: export Verdict, VerdictLabel, Confidence and Depth Literal aliases For comparisons, exhaustive matching and docs, named like the Node SDK's. The model fields and method arguments stay str, so a value a later API adds still reads and still sends. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/__init__.py | 8 ++++++++ src/lenz_io/models.py | 22 ++++++++++++++++++++++ tests/test_literal_aliases.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 tests/test_literal_aliases.py diff --git a/src/lenz_io/__init__.py b/src/lenz_io/__init__.py index 5e87e4a..af3f3ab 100644 --- a/src/lenz_io/__init__.py +++ b/src/lenz_io/__init__.py @@ -91,10 +91,12 @@ Citecheck, CitecheckStarted, ClaimLocation, + Confidence, Coverage, CoverageReason, CoverageStatus, DebateSide, + Depth, EntityRef, Escalation, EscalationPolicy, @@ -128,6 +130,8 @@ UsageCapacity, UsageCredits, UsageExtract, + Verdict, + VerdictLabel, Verification, VerificationList, VerificationListItem, @@ -171,10 +175,12 @@ "CitecheckTimeout", "CitecheckTimeoutError", "ClaimLocation", + "Confidence", "Coverage", "CoverageReason", "CoverageStatus", "DebateSide", + "Depth", "EntityRef", "Escalation", "EscalationPolicy", @@ -232,6 +238,8 @@ "UsageCapacity", "UsageCredits", "UsageExtract", + "Verdict", + "VerdictLabel", "Verification", "VerificationCompleted", "VerificationFailed", diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 9a67dbb..5e31386 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -141,6 +141,28 @@ def _sent(model: BaseModel, key: str) -> Any: ] +#: The five verdict labels a check can return. A documentation and comparison +#: alias, like ``FailureClass``: the ``verdict`` fields stay ``str``, so a label +#: the API adds later still reads. +VerdictLabel = Literal["True", "Mostly True", "Mixed", "Mostly False", "False"] + +#: A verdict as read on a verification or an assess row: one of the five +#: labels, or ``"Error"`` on a failed assess row (deprecated there: read +#: ``status == "failed"``). Fields stay ``str``:: +#: +#: from lenz_io import Verdict +#: FALSE_ISH: set[Verdict] = {"False", "Mostly False"} +#: if row.verdict in FALSE_ISH: ... +Verdict = Literal["True", "Mostly True", "Mixed", "Mostly False", "False", "Error"] + +#: A confidence band, as read on a verification or an assess row. +Confidence = Literal["low", "medium", "high"] + +#: A check's depth: ``"standard"`` (10 credits) or ``"low"`` (5 credits, +#: fewer sources). The ``depth=`` arguments stay ``str``. +Depth = Literal["standard", "low"] + + class Source(_Lax): """A single citation backing a verification.""" diff --git a/tests/test_literal_aliases.py b/tests/test_literal_aliases.py new file mode 100644 index 0000000..871196e --- /dev/null +++ b/tests/test_literal_aliases.py @@ -0,0 +1,32 @@ +"""``Verdict``, ``VerdictLabel``, ``Confidence`` and ``Depth``: the accepted +values as exported ``Literal`` aliases, for comparisons, exhaustive matching +and docs. The model fields and method arguments stay ``str``, so a value a +later API adds still reads and still sends.""" + +from __future__ import annotations + +import inspect +from typing import get_args + +import lenz_io +from lenz_io import AssessClaim, Confidence, Depth, Lenz, Verdict, VerdictLabel, Verification + + +def test_the_values() -> None: + assert get_args(VerdictLabel) == ("True", "Mostly True", "Mixed", "Mostly False", "False") + assert get_args(Verdict) == ("True", "Mostly True", "Mixed", "Mostly False", "False", "Error") + assert get_args(Confidence) == ("low", "medium", "high") + assert get_args(Depth) == ("standard", "low") + + +def test_exported() -> None: + for name in ("Verdict", "VerdictLabel", "Confidence", "Depth"): + assert name in lenz_io.__all__ + + +def test_fields_and_arguments_stay_open_strings() -> None: + for model in (Verification, AssessClaim): + assert model.model_fields["verdict"].annotation is str + assert model.model_fields["confidence"].annotation is str + assert Verification.model_validate({"verdict": "Unproven", "confidence": "unknown"}).verdict == "Unproven" + assert inspect.signature(Lenz.verify).parameters["depth"].annotation == "str" From 6942413e87626292fcd8b4655d1921338bc3c441 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:08:21 +0300 Subject: [PATCH 26/67] feat(webhooks): verification events carry .verification, the status envelope VerificationCompleted, VerificationFailed and VerificationNeedsInput expose .verification: the verification as client.get_status returns it (a typed TaskStatus, the verdict under .verification.result), built from either payload shape, None when the payload cannot be read as one. A property, so the events' fields and repr are unchanged; the dict result stays. A malformed nested result no longer crashes parse_webhook. The FastAPI example reads it and handles review and citation-check events. Co-Authored-By: Claude Opus 5.5 --- examples/core/fastapi_webhook.py | 53 ++++++++++++++---- src/lenz_io/webhooks.py | 75 ++++++++++++++++++++++++- tests/test_webhook_verification.py | 90 ++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 tests/test_webhook_verification.py diff --git a/examples/core/fastapi_webhook.py b/examples/core/fastapi_webhook.py index 9454436..b647c5a 100644 --- a/examples/core/fastapi_webhook.py +++ b/examples/core/fastapi_webhook.py @@ -1,7 +1,7 @@ """Receive Lenz webhook events in a FastAPI app. -Lenz POSTs HMAC-signed payloads to your ``webhook_url`` when the -verification pipeline terminates. This handler verifies the signature, +Lenz POSTs HMAC-signed payloads to your ``webhook_url`` when a +verification, a review or a citation check ends. This handler verifies the signature, parses the payload into a typed event, and dispatches per event type. Run: @@ -22,8 +22,10 @@ from fastapi import FastAPI, HTTPException, Request from lenz_io import ( + CitecheckEvent, LenzWebhooks, LenzWebhookSignatureError, + ReviewEvent, VerificationCompleted, VerificationFailed, VerificationNeedsInput, @@ -45,23 +47,50 @@ async def lenz_webhook(request: Request) -> dict[str, str]: logger.warning("Rejected webhook: %s", exc.message) raise HTTPException(status_code=400, detail=str(exc)) from exc + # A delivery can be retried: deduplicate on event.event_id (stable across + # attempts) before acting on it. if isinstance(event, VerificationCompleted): - # Verdict block is FLAT on event.result — no nested object. - logger.info( - "Completed: %s -> %s (lenz_score %s, confidence %s)", - event.verification_id, - event.result.get("verdict"), - event.result.get("lenz_score"), - event.result.get("confidence"), - ) + # The verification as client.get_status returns it; the verdict is + # under .result. + result = event.verification.result if event.verification else None + if result is not None: + logger.info( + "Completed: %s -> %s (lenz_score %s, confidence %s)", + result.verification_id, + result.verdict, + result.lenz_score, + result.confidence, + ) # TODO: persist verdict + sources to your DB; ping users; etc. elif isinstance(event, VerificationNeedsInput): - logger.info("Needs input on %s: %s", event.task_id, event.needs_input.get("reason")) + logger.info("Needs input on %s: %s", event.task_id, [c.claim for c in event.claims]) # TODO: surface the candidate claims to the user, then call # client.select(task_id, claims=[...]) to resolve. elif isinstance(event, VerificationFailed): - logger.warning("Pipeline failed: %s (%s)", event.task_id, event.error) + failure = event.failure + logger.warning("Verification failed: %s (%s)", event.task_id, failure.code if failure else "unknown") + elif isinstance(event, ReviewEvent): + # review.completed / review.failed: the final review, as get_review returns it. + if event.review is not None: + logger.info( + "Review %s %s: outcome %s, %d issue(s)", + event.review_id, + event.status, + event.review.outcome, + len(event.review.issues), + ) + elif isinstance(event, CitecheckEvent): + # citecheck.completed / citecheck.failed: the final check. + if event.citecheck is not None: + logger.info( + "Citation check %s %s: outcome %s, %d issue(s)", + event.citecheck_id, + event.status, + event.citecheck.outcome, + len(event.citecheck.citation_issues), + ) else: + # A new event type: acknowledge it and move on. logger.info("Unhandled webhook event: %s", event.event) # Always return 2xx fast. Lenz expects an ack within 5s; otherwise the diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index 5234d50..5aadff1 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -43,7 +43,9 @@ Citecheck, FailureBlock, ReviewFull, + TaskStatus, _fill_modified_at, + _new_code, _old_code, _verification_failure, ) @@ -122,12 +124,67 @@ def event_id(self) -> str: return value if isinstance(value, str) else "" +def _status_envelope(event: WebhookEvent) -> TaskStatus | None: + """The verification of a ``verification.*`` event as ``client.get_status`` + returns it: the newer payload's ``verification``, else one built from the + original flat fields. ``None`` when the payload cannot be read as one.""" + raw = event.raw + nested = raw.get("verification") + body: dict[str, Any] + if isinstance(nested, dict): + # A ``null`` reads as the field left out (``hint: null`` on a pause). + body = {k: v for k, v in nested.items() if v is not None} + elif event.event == "verification.completed": + body = {"status": "completed", "task_id": raw.get("task_id") or ""} + if raw.get("result") is not None: + body["result"] = raw["result"] + elif event.event == "verification.failed": + error = raw.get("error") + failure: dict[str, Any] = { + "code": _new_code(error) if isinstance(error, str) else "", + # The original payload's ``error`` is the code, never a sentence. + "detail": None, + "failure_class": raw.get("failure_class"), + "retryable": raw.get("retryable") if isinstance(raw.get("retryable"), bool) else None, + } + body = { + "status": "failed", + "task_id": raw.get("task_id") or "", + "error": "", + "failure_reason": error if isinstance(error, str) else "", + "failure": failure, + } + else: + needs_input = raw.get("needs_input") + given = needs_input if isinstance(needs_input, dict) else {} + body = {"status": "needs_input", "task_id": raw.get("task_id") or ""} + body.update({k: v for k, v in given.items() if v is not None}) + try: + return TaskStatus.model_validate(body) + except ValidationError: + return None + + @dataclass class VerificationCompleted(WebhookEvent): - """``event=verification.completed`` — the pipeline produced a verdict.""" + """``event=verification.completed`` — the pipeline produced a verdict. + + Read :attr:`verification`: the verification as ``client.get_status`` + returns it, the verdict under ``verification.result`` (a typed + :class:`Verification`). ``result`` (the original payload's flat dict) is + deprecated and kept. + """ result: dict[str, Any] = field(default_factory=dict) + @property + def verification(self) -> TaskStatus | None: + """The verification as ``client.get_status`` returns it, from either + payload shape: ``status``, ``task_id`` and ``result`` (the verdict, + a typed :class:`Verification`; ``None`` when the payload carried + none). ``None`` when the payload cannot be read as one. Since 3.0.""" + return _status_envelope(self) + @dataclass class VerificationFailed(WebhookEvent): @@ -143,6 +200,13 @@ class VerificationFailed(WebhookEvent): failure_class: str = "" retryable: bool | None = None + @property + def verification(self) -> TaskStatus | None: + """The verification as ``client.get_status`` returns it, from either + payload shape: ``status``, ``task_id`` and ``failure``. ``None`` when + the payload cannot be read as one. Since 3.0.""" + return _status_envelope(self) + @property def failure(self) -> FailureBlock | None: """Why the run failed, from either payload shape: ``code`` (the cause, @@ -178,6 +242,13 @@ class VerificationNeedsInput(WebhookEvent): needs_input: dict[str, Any] = field(default_factory=dict) hint: str = "" + @property + def verification(self) -> TaskStatus | None: + """The verification as ``client.get_status`` returns it, from either + payload shape: ``status``, ``task_id``, ``reason``, ``claims`` and + ``hint``. ``None`` when the payload cannot be read as one. Since 3.0.""" + return _status_envelope(self) + @property def reason(self) -> str: """Why the run paused (``multi_claim``).""" @@ -341,7 +412,7 @@ def _original_view(payload: dict[str, Any]) -> dict[str, Any]: view.setdefault( "needs_input", {"reason": body.get("reason", ""), "claims": options, "hint": body.get("hint", "")} ) - view.setdefault("coverage", body.get("coverage") or (result or {}).get("coverage")) + view.setdefault("coverage", body.get("coverage") or (result if isinstance(result, dict) else {}).get("coverage")) return view diff --git a/tests/test_webhook_verification.py b/tests/test_webhook_verification.py new file mode 100644 index 0000000..c43fb84 --- /dev/null +++ b/tests/test_webhook_verification.py @@ -0,0 +1,90 @@ +"""``.verification`` on the ``verification.*`` webhook events: the +verification as ``client.get_status`` returns it (a ``TaskStatus``; on a +completed event the verdict is ``.verification.result``), built from either +payload shape, as in the Node SDK.""" + +from __future__ import annotations + +import pytest +from parity_observe import load, names + +from lenz_io import ( + TaskStatus, + Verification, + VerificationCompleted, + VerificationFailed, + VerificationNeedsInput, + WebhookEvent, + parse_webhook, +) + +_VERIFICATION_EVENTS = [n for n in names() if n.startswith("webhook__verification_")] + + +def _both(name: str) -> tuple[WebhookEvent, WebhookEvent]: + return parse_webhook(load("legacy", name)["body"]), parse_webhook(load("canonical", name)["body"]) + + +@pytest.mark.parametrize("name", _VERIFICATION_EVENTS) +def test_both_shapes_give_the_same_status_envelope(name: str) -> None: + old, new = _both(name) + assert type(old) is type(new) + a, b = old.verification, new.verification # type: ignore[attr-defined] + assert isinstance(a, TaskStatus) and isinstance(b, TaskStatus) + assert a.status == b.status == old.status + assert a.task_id == b.task_id == old.task_id + if isinstance(old, VerificationCompleted): + assert isinstance(a.result, Verification) and isinstance(b.result, Verification) + for field in ("verification_id", "claim", "verdict", "confidence", "lenz_score", "key_finding"): + assert getattr(a.result, field) == getattr(b.result, field), field + assert a.result.verification_id == old.verification_id + assert a.result.verdict == old.result.get("verdict") + elif isinstance(old, VerificationFailed): + assert a.failure is not None and b.failure is not None + for field in ("code", "failure_class", "retryable"): + assert getattr(a.failure, field) == getattr(b.failure, field), field + assert a.failure_reason == old.error + assert a.retryable == old.retryable + else: + assert isinstance(old, VerificationNeedsInput) + assert a.reason == b.reason == old.reason + assert [c.claim for c in a.claims] == [c.claim for c in b.claims] == [c.claim for c in old.claims] + assert a.hint == b.hint == old.hint + + +def test_it_is_a_property_so_the_event_repr_and_fields_are_unchanged() -> None: + event = parse_webhook(load("canonical", _VERIFICATION_EVENTS[0])["body"]) + assert "verification" not in vars(event) + assert "verification=" not in repr(event) + + +@pytest.mark.parametrize( + "payload", + [ + {"event": "verification.completed", "task_id": "t", "verification": {"status": "completed", "result": 5}}, + {"event": "verification.completed", "task_id": "t", "result": "not an object"}, + {"event": "verification.failed", "task_id": "t", "verification": {"status": ["failed"]}}, + {"event": "verification.needs_input", "task_id": "t", "needs_input": {"claims": "x"}}, + ], +) +def test_a_malformed_recognised_event_reads_none_never_raises(payload: dict) -> None: + event = parse_webhook(payload) + assert event.verification is None # type: ignore[attr-defined] + + +def test_a_completed_event_without_a_result_reads_a_status_without_one() -> None: + event = parse_webhook({"event": "verification.completed", "task_id": "t"}) + assert isinstance(event, VerificationCompleted) + assert event.verification is not None + assert event.verification.status == "completed" + assert event.verification.task_id == "t" + assert event.verification.result is None + + +def test_other_events_do_not_carry_it() -> None: + for payload in ( + {"event": "certificate.timestamped", "task_id": "t"}, + {"event": "review.completed", "review_id": "r"}, + {"event": "something.new", "verification": {"status": "completed"}}, + ): + assert not hasattr(parse_webhook(payload), "verification") From 25036cbeefba459aa08e1e7b3dac8169b299a154 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:09:21 +0300 Subject: [PATCH 27/67] feat: verifications.iter() and library.iter() walk every page Lazily, one page when its first item is asked for, from the start page, with the page size read from each response, stopping after a short or empty page. library.iter takes list's filters and refuses sort='random', which is not exhaustive. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 66 ++++++++++++++++- tests/test_pagination_iterators.py | 111 +++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 tests/test_pagination_iterators.py diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index a2cf598..8a55c26 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -77,11 +77,12 @@ from __future__ import annotations +import builtins import logging import os import time import uuid -from collections.abc import Callable +from collections.abc import Callable, Iterator from typing import Any, Literal, TypedDict, TypeVar, overload import httpx @@ -119,6 +120,7 @@ Citecheck, CitecheckStarted, ExtractedClaims, + LibraryItem, LibraryList, Progress, RelatedVerifications, @@ -130,6 +132,7 @@ Usage, Verification, VerificationList, + VerificationListItem, ) logger = logging.getLogger("lenz_io") @@ -317,6 +320,21 @@ def list(self, *, page: int = 1) -> VerificationList: body = self._p._request("GET", "/verifications", params={"page": page}) return VerificationList.model_validate(body) + def iter(self, *, page: int = 1) -> Iterator[VerificationListItem]: + """Every verification on your account, newest first, page after page + from ``page`` (default 1). + + Fetches a page only when the items before it have been consumed, reads + the page size from each response, and stops after a short or empty + page:: + + for item in client.verifications.iter(): + print(item.verification_id, item.verdict) + + Since 3.0. ``list(page=...)`` reads one page. + """ + return _walk(lambda n: self.list(page=n), page) + def get(self, verification_id: str) -> Verification: """Fetch a single verification by id. @@ -524,6 +542,52 @@ def list( ) return LibraryList.model_validate(body) + def iter( + self, + *, + page: int = 1, + sort: str = "recent", + search: str = "", + domain: str = "", + entity: str = "", + # ``builtins.list``: in this class body ``list`` is the method above. + curated: builtins.list[str] | None = None, + verdict: str = "", + ) -> Iterator[LibraryItem]: + """Every item of the public catalog that matches the filters (the + ones ``list`` takes), page after page from ``page``. Works without an + API key. + + Fetches a page only when the items before it have been consumed, reads + the page size from each response, and stops after a short or empty + page. ``sort="random"`` raises ``ValueError``: a random order is drawn + anew for every page, so walking it neither reaches every item nor + avoids repeats (use ``list(sort="random")`` for a sample). Since 3.0. + """ + if sort == "random": + raise ValueError( + "library.iter() does not take sort='random': a random order is not exhaustive. " + "Use library.list(sort='random') for a sample." + ) + return _walk( + lambda n: self.list( + page=n, sort=sort, search=search, domain=domain, entity=entity, curated=curated, verdict=verdict + ), + page, + ) + + +def _walk(fetch: Callable[[int], VerificationList | LibraryList], page: int) -> Iterator[Any]: + """The items of ``fetch(page)``, ``fetch(page + 1)``, ... until a page + comes back shorter than its own ``page_size`` (or empty). A generator: no + page is fetched before its first item is asked for.""" + while True: + current = fetch(page) + yield from current.items + if not current.items or len(current.items) < current.page_size: + return + page += 1 + def _call_key(idempotency_key: str | None, idempotency: bool) -> str | None: """The ``Idempotency-Key`` for one call: the caller's own, else a random diff --git a/tests/test_pagination_iterators.py b/tests/test_pagination_iterators.py new file mode 100644 index 0000000..6941056 --- /dev/null +++ b/tests/test_pagination_iterators.py @@ -0,0 +1,111 @@ +"""``verifications.iter()`` and ``library.iter()``: every item, page after +page. The page size is read from each response (the API takes none on the +request), the start page is honoured, the walk ends on a short or empty page, +a page is fetched only when the items before it have been consumed, and +``sort="random"`` (not exhaustive) is refused.""" + +from __future__ import annotations + +from itertools import islice + +import pytest +import respx + +from lenz_io import Lenz, LibraryItem, VerificationListItem + +BASE = "https://lenz.io/api/v1" + + +def _page(start: int, count: int, page: int, page_size: int, total: int = 100) -> dict: + items = [{"verification_id": f"v{start + i}", "claim": f"C{start + i}."} for i in range(count)] + return {"items": items, "total": total, "page": page, "page_size": page_size} + + +def _pager(route: respx.Route, pages: dict[int, dict]) -> None: + def respond(request): + import httpx + + return httpx.Response(200, json=pages[int(request.url.params["page"])]) + + route.side_effect = respond + + +@pytest.mark.parametrize( + ("path", "walk", "item_type"), + [ + ("/verifications", lambda c, **kw: c.verifications.iter(**kw), VerificationListItem), + ("/library", lambda c, **kw: c.library.iter(**kw), LibraryItem), + ], +) +class TestIter: + def test_every_page_until_a_short_one(self, client: Lenz, path, walk, item_type) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {1: _page(0, 2, 1, 2), 2: _page(2, 2, 2, 2), 3: _page(4, 1, 3, 2)}) + items = list(walk(client)) + assert [i.verification_id for i in items] == ["v0", "v1", "v2", "v3", "v4"] + assert all(isinstance(i, item_type) for i in items) + assert [c.request.url.params["page"] for c in route.calls] == ["1", "2", "3"] + assert all("page_size" not in c.request.url.params for c in route.calls) + + def test_a_full_last_page_ends_on_the_empty_one_after_it(self, client: Lenz, path, walk, item_type) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {1: _page(0, 2, 1, 2), 2: _page(0, 0, 2, 2)}) + assert len(list(walk(client))) == 2 + assert route.call_count == 2 + + def test_the_page_size_is_read_from_each_response(self, client: Lenz, path, walk, item_type) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {1: _page(0, 3, 1, 3), 2: _page(3, 3, 2, 5)}) + assert len(list(walk(client))) == 6 + assert route.call_count == 2 + + def test_the_start_page_is_honoured(self, client: Lenz, path, walk, item_type) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {3: _page(40, 1, 3, 20)}) + items = list(walk(client, page=3)) + assert [i.verification_id for i in items] == ["v40"] + assert route.calls.last.request.url.params["page"] == "3" + + def test_an_empty_first_page(self, client: Lenz, path, walk, item_type) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {1: _page(0, 0, 1, 20, total=0)}) + assert list(walk(client)) == [] + assert route.call_count == 1 + + def test_no_prefetch(self, client: Lenz, path, walk, item_type) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {1: _page(0, 2, 1, 2), 2: _page(2, 2, 2, 2)}) + it = walk(client) + assert route.call_count == 0, "nothing is fetched before the first item is asked for" + assert [i.verification_id for i in islice(it, 2)] == ["v0", "v1"] + assert route.call_count == 1 + next(it) + assert route.call_count == 2 + + +class TestFilters: + def test_library_filters_reach_every_page(self, unauth_client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get("/library") + _pager(route, {1: _page(0, 2, 1, 2), 2: _page(2, 0, 2, 2)}) + list(unauth_client.library.iter(sort="most_true", search="moon", curated=["trivia"], verdict="True")) + for call in route.calls: + params = call.request.url.params + assert params["sort"] == "most_true" + assert params["search"] == "moon" + assert params["curated"] == "trivia" + assert params["verdict"] == "True" + assert "Authorization" not in call.request.headers + + def test_random_order_is_refused(self, unauth_client: Lenz) -> None: + with respx.mock(base_url=BASE, assert_all_called=False) as r: + route = r.get("/library") + with pytest.raises(ValueError, match="random"): + unauth_client.library.iter(sort="random") + assert route.call_count == 0 From 0798fe7c49fc70eccb8977c2f5876facbb693101 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:10:08 +0300 Subject: [PATCH 28/67] feat: verify, review_and_wait and citecheck_and_wait list every option The options they forwarded through **kwargs are keyword-only parameters with the same defaults, so editors complete them and type checkers check them. Positional claim/text and every default are unchanged, the request bodies match the frozen baseline, and an unknown option is still a TypeError. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 59 +++++++++++++++++++++++++++----- tests/test_public_call_bodies.py | 36 +++++++++++++++++++ 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 8a55c26..008f789 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -670,7 +670,8 @@ def verify( depth: str = "", idempotency: bool = True, idempotency_key: str | None = None, - **kwargs: Any, + source_url: str = "", + webhook_url: str = "", ) -> TaskAccepted: """Submit a claim for verification. Returns a ``task_id``; the pipeline runs async. For sync ergonomics use ``verify_and_wait``. @@ -700,15 +701,21 @@ def verify( ``idempotency_key=`` to make a retry from a different process replay too, or pass ``idempotency=False`` to send none. Never derived from the claim: the same claim sent again later is a new verification. + + ``source_url`` (optional): where the claim was found, kept with the + verification. ``webhook_url`` (optional): where to send this + verification's ``verification.*`` events; leave it out (or blank) for + your key's default webhook URL. """ return self._verify_submit( claim=claim, text=text, + source_url=source_url, + webhook_url=webhook_url, language=language, visibility=visibility, depth=depth, idempotency_key=_call_key(idempotency_key, idempotency), - **kwargs, ) def verify_batch( @@ -1302,9 +1309,20 @@ def review_and_wait( *, timeout: float = 600.0, on_update: Callable[[ReviewFull], None] | None = None, - **kw: Any, + verdicts: list[str] | None = None, + confidence: list[str] | None = None, + max_assessments: int | None = None, + max_verifications: int | None = None, + depth: str | None = None, + max_citations: int | None = None, + suggest_edits: bool = False, + language: str = "", + webhook_url: str | None = None, + visibility: str = "private", + idempotency_key: str | None = None, ) -> ReviewFull: - """Start a review (``review(text, **kw)``) and poll it until it ends. + """Start a review (``review(text, ...)``, which documents every option + but ``timeout`` and ``on_update``) and poll it until it ends. Returns the completed ``ReviewFull``: read ``outcome``, then ``issues``. Polls on the review's own ``poll_after_seconds`` (never @@ -1318,7 +1336,20 @@ def review_and_wait( ``timeout`` seconds pass first: the review keeps running, and the error carries its ``review_id`` and the last body read. """ - started = self.review(text, **kw) + started = self.review( + text, + verdicts=verdicts, + confidence=confidence, + max_assessments=max_assessments, + max_verifications=max_verifications, + depth=depth, + max_citations=max_citations, + suggest_edits=suggest_edits, + language=language, + webhook_url=webhook_url, + visibility=visibility, + idempotency_key=idempotency_key, + ) logger.info("Submitted review: %s", started.review_id) return self._wait_review(started.review_id, timeout=timeout, on_update=on_update) @@ -1397,9 +1428,14 @@ def citecheck_and_wait( *, timeout: float = 600.0, on_update: Callable[[Citecheck], None] | None = None, - **kw: Any, + pairs: list[CitationPair] | None = None, + max_citations: int | None = None, + language: str = "", + webhook_url: str | None = None, + idempotency_key: str | None = None, ) -> Citecheck: - """Start a citation check (``citecheck(text, **kw)``) and poll it until + """Start a citation check (``citecheck(text, ...)``, which documents + every option but ``timeout`` and ``on_update``) and poll it until it ends. Returns the completed :class:`Citecheck`: read ``outcome``, then ``citation_issues``. @@ -1410,7 +1446,14 @@ def citecheck_and_wait( first: the check keeps running, and the error carries its ``citecheck_id`` and the last body read. """ - started = self.citecheck(text, **kw) + started = self.citecheck( + text, + pairs=pairs, + max_citations=max_citations, + language=language, + webhook_url=webhook_url, + idempotency_key=idempotency_key, + ) logger.info("Submitted citation check: %s", started.citecheck_id) return self._wait_citecheck(started.citecheck_id, timeout=timeout, on_update=on_update) diff --git a/tests/test_public_call_bodies.py b/tests/test_public_call_bodies.py index faa12bf..af4226c 100644 --- a/tests/test_public_call_bodies.py +++ b/tests/test_public_call_bodies.py @@ -386,3 +386,39 @@ def test_citecheck_and_wait( client.citecheck_and_wait(*args, idempotency_key=KEY, **kwargs) assert _ordered(route.calls.last.request.content) == expected assert route.calls.last.request.headers["Idempotency-Key"] == KEY + + +# ── 3.0: every forwarded option is a named, keyword-only parameter ───────── + + +@pytest.mark.parametrize( + ("outer", "inner", "own"), + [ + ("verify", "_verify_submit", {"claim", "text", "idempotency", "idempotency_key"}), + ("review_and_wait", "review", {"text", "timeout", "on_update"}), + ("citecheck_and_wait", "citecheck", {"text", "timeout", "on_update"}), + ], +) +def test_forwarded_options_are_listed(outer: str, inner: str, own: set[str]) -> None: + import inspect + + outer_params = inspect.signature(getattr(Lenz, outer)).parameters + inner_params = inspect.signature(getattr(Lenz, inner)).parameters + assert not any(p.kind is inspect.Parameter.VAR_KEYWORD for p in outer_params.values()), "no **kwargs" + forwarded = {n for n in inner_params if n != "self"} - own + for name in forwarded: + assert name in outer_params, name + assert outer_params[name].kind is inspect.Parameter.KEYWORD_ONLY, name + assert outer_params[name].default == inner_params[name].default, name + first = next(n for n in outer_params if n != "self") + assert outer_params[first].kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + + +def test_an_unknown_option_is_still_a_type_error(client: Lenz) -> None: + for call in ( + lambda: client.verify("A.", sorce_url="x"), # type: ignore[call-arg] + lambda: client.review_and_wait("Draft.", max_asessments=1), # type: ignore[call-arg] + lambda: client.citecheck_and_wait("Draft.", max_citation=1), # type: ignore[call-arg] + ): + with pytest.raises(TypeError): + call() From acaa62f30e08ef8c7f4d57f04781e5761f2c1643 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:13:47 +0300 Subject: [PATCH 29/67] docs: a first call at the top, runnable review and citecheck examples, the 3.0 changes The README opens with a ten-line first call (key, assess, verdict and confidence); the CLI moves below the review and citation-check sections. The quickstarts no longer fail when no claim was escalated, read failed rows by status == "failed", and give depth="low" its real price; one count for the API's calls. New examples/core/review_draft.py and citecheck_draft.py, and CI type-checks every example (which caught quickstart.py reading a reply field that does not exist). The README and CHANGELOG cover the new errors, retryable, waits, automatic keys, iterators, webhook .verification and the Literal aliases, with an upgrade box and the replay note. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 +- CHANGELOG.md | 87 +++++++++ README.md | 285 ++++++++++++++++++++--------- examples/core/citecheck_draft.py | 57 ++++++ examples/core/fastapi_webhook.py | 1 + examples/core/quickstart.py | 27 +-- examples/core/review_draft.py | 66 +++++++ examples/core/verify_llm_output.py | 8 +- scripts/hooks/pre-commit | 3 +- src/lenz_io/__init__.py | 20 +- src/lenz_io/client.py | 19 +- 11 files changed, 458 insertions(+), 121 deletions(-) create mode 100644 examples/core/citecheck_draft.py create mode 100644 examples/core/review_draft.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 172ba43..8308bc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,11 @@ jobs: ruff format --check . - name: Type-check # mypy strict only on hand-written code; generated _generated/ (if added later) is excluded via pyproject. - run: mypy src/lenz_io + # The examples too: they are what people copy, so a renamed field or a + # wrong type there fails CI like one in the SDK. + run: | + mypy src/lenz_io + mypy examples - name: Test # `--no-cov` because the coverage gate lives in `addopts` and would # otherwise run in all eight matrix cells. Coverage does not vary diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a1f61..de6a071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ All notable changes to this SDK are documented here. Format follows ## [Unreleased] +> **Upgrading from 2.x.** Must do: (1) upgrade every service that *receives* +> your webhooks to lenz-io 2.21+ before the sending service moves to 3.0; +> (2) code reading raw bodies (`exc.body`, `event.raw`) reads the current +> shape; (3) re-record tests that replay recorded 2.x response bodies; (4) an +> idempotent request first sent before the switch and replayed after it raises +> `LenzApiVersionError`: finish that work with 2.x, never change the key to +> get past it. May do: move off the deprecated names (they keep working). +> Details under Migration. + Major release (3.0.0). The SDK asks for the API's current response shape (`2026-10-11`) and reads only that shape from its own calls. Every attribute, exception and CLI rendering keeps the value it had in 2.x (computed from the @@ -93,6 +102,76 @@ Deprecated). 429; `LenzQuotaExceededError.credit_balance` on a citation-check 402. - `map_response_to_error` takes an optional `endpoint=(method, path)`, which the client passes: an error's original `code` and wording depend on it. +- **Batch submit and `ask.send` send an automatic `Idempotency-Key`.** + `verify_batch`, `verify_batch_and_wait` and `ask.send` generate one random + key per call, reused across that call's own retries (as `verify`, `assess` + and `extract` already did), so a retried batch or question replays the + first answer instead of being charged twice. A key you pass wins; + `idempotency=False` sends none. Asking the same question again is a new + call with a new key, so it is asked again. A retry that lands while the + first attempt still runs gets the 409 it got before; the SDK never sends a + second key to get past it. 2.x sent a key there only when you passed one. +- **`wait`, `verify_and_wait` and `verify_batch_and_wait` stop at once on a + 401, 403 or 404** (raising `LenzAuthError` / `LenzNotFoundError`; in a + batch, that item is `failed` with no `status_detail` and the others keep + going) instead of polling to a misleading `LenzTimeoutError`. A 5xx, a 429 + or a network failure is still polled again, after the wait it stated. Each + poll is one request bounded by what is left of the deadline (at least 1s, + at most the client timeout); the client's own retry ladder no longer runs + inside a poll. +- **Network failures and transport timeouts raise subclasses of the class + they raised before**: `LenzConnectionError` and `LenzRequestTimeoutError` + (a `LenzConnectionError`), both `LenzAPIError`s, with the same message and + the `httpx` exception as `__cause__`. +- **A 404 raises `LenzNotFoundError`** (a `LenzError`, as before) and its + `fix` reads "Check the id the request names and the API key it was sent + with: retrying will not find it." (2.x advised retrying). The message, the + other fields and every other error's text are unchanged. + +### Added + +- `LenzNotFoundError` (404), `LenzConnectionError` and + `LenzRequestTimeoutError` (see Changed). +- **`retryable` on every error**, set when the error is built: whether sending + the same request again can succeed. `True` for a connection failure, a + request timeout, a 429 and a 5xx; `False` for any other 4xx, a + `LenzApiVersionError` and the client-side errors a resend cannot fix (a + missing key, a `*_and_wait` timeout, a needs-input pause, a bad webhook + signature); `None` when the SDK cannot say. A failed verification, review + or citation check keeps the server's value (`None` when it sent none), and a + boolean `retryable` in a response's `failure` block always wins. A + `retryable=` passed to an error's constructor wins too. +- `ReviewFailedError`, `ReviewTimeoutError`, `CitecheckFailedError` and + `CitecheckTimeoutError`: the job errors under the names the Node SDK uses + (the same classes as `ReviewFailed`, `ReviewTimeout`, `CitecheckFailed`, + `CitecheckTimeout`). +- **`verifications.iter()` and `library.iter(**filters)`**: every item, page + after page from `page`, fetched lazily (a page only when its first item is + asked for), the page size read from each response, ending after a short or + empty page. `library.iter` takes `list`'s filters and refuses + `sort="random"` (`ValueError`), which is not exhaustive. +- **`.verification` on the `verification.completed`, `verification.failed` + and `verification.needs_input` webhook events**: the verification as + `client.get_status` returns it (a `TaskStatus`; on a completed event the + verdict is `.verification.result`, a typed `Verification`), built from + either payload shape, `None` when a payload cannot be read as one. A + property, so the events' fields and `repr` are unchanged; the dict + `result` stays (prefer `.verification`). A recognised event whose nested + `result` is not an object no longer crashes `parse_webhook`. +- `Verdict`, `VerdictLabel`, `Confidence` and `Depth`: `Literal` aliases of + the accepted values, for comparisons and exhaustive matching. Fields and + arguments stay `str`. +- `verify`, `review_and_wait` and `citecheck_and_wait` list every option they + forward (`source_url` and `webhook_url`; every `review` / `citecheck` + option) as keyword-only parameters with the same defaults, instead of + `**kwargs`, so editors complete them and type checkers check them. Request + bodies are unchanged, and an unknown option is still a `TypeError`. +- A "First call" at the top of the README, and runnable review and + citation-check examples (`examples/core/review_draft.py`, + `examples/core/citecheck_draft.py`); CI type-checks every example. The + quickstarts no longer fail when no claim needed escalating, read failed + rows by `status == "failed"`, and give `depth="low"`'s price (5 credits, not + 10) everywhere. ### Deprecated @@ -198,6 +277,14 @@ rebuild: - **Tests with recorded 2.x response bodies** must be re-recorded: the SDK reads the current shape only, so a stored 2.x body no longer parses into the values it did. +- **Replays of requests made before the switch.** An idempotent request + first sent before lenz.io served `2026-10-11` and replayed with the same + `Idempotency-Key` afterwards answers with its stored reply in + `2026-05-13`, which 3.x refuses with `LenzApiVersionError`. Replays are + kept for 24 hours and replies stored since 2026-10-09 are kept in both + versions, so in practice none remain at release. If you meet one, finish + that work with 2.x; never change the key to get past it, which would run + (and charge) the request again. - **The values the API no longer sends** (see Breaking): `chain_id`, the review / citecheck webhook `task_id`, reworded failure sentences and hints, and an `/extract` status on a mixed input. diff --git a/README.md b/README.md index 8b238dc..33da51d 100644 --- a/README.md +++ b/README.md @@ -16,80 +16,27 @@ generators, deep-research products, due-diligence platforms, vertical agents producing structured deliverables. Not chat AI, not voice AI, not real-time copilots — pipeline runs are the wrong shape for those. -```bash -pip install lenz-io -``` - -## Command-line tool +## First call -The same primitives from your terminal — submit, poll, and read full reports. -Ships inside this package behind the `cli` extra (quotes matter — bare brackets -are a glob in zsh): - -```bash -pipx install "lenz-io[cli]" # isolated CLI install (recommended) -pip install "lenz-io[cli]" # or into your current environment -``` +Get a free API key, with free credits to start, at +[lenz.io/api-credentials](https://lenz.io/api-credentials), then: ```bash -lenz login # paste an API key (free — get one at lenz.io/api-credentials) -lenz extract "Einstein won the 1921 Nobel for relativity" # free, 1000/day -lenz extract "$(cat deck.txt)" --focus "market size" # only the claims you want -lenz extract "$(cat draft.txt)" --locate # where the text makes each claim -lenz assess "The Great Wall is visible from space" # fast verdict -lenz assess "" "" "" # one call, one verdict per claim (up to 20) -lenz verify "Water boils at 90C at sea level" # full pipeline (~90s) -lenz verify "" --depth low # shallower, faster, half the credits -lenz verify "" --json | jq .verdict # machine-readable -lenz status # non-blocking: poll a verify task's progress -lenz show # full report — sources, warnings, panel + debate (-c for concise) -lenz ask "Which source is strongest?" -lenz review draft.md # the whole draft: quick verdicts, deep checks, and up to 20 of its sources -lenz review draft.md --issues # only the issues -lenz review draft.md --max-citations 0 # claims only, no source checked -lenz review draft.md --max-assessments 0 # only the sources, no claim -lenz citecheck draft.md # the citation check on its own -lenz citecheck --pairs pairs.json # statement-source pairs, each checked as it is -lenz usage # credits left, what they buy, and when they reset -lenz config # show which key/base URL is in use -``` - -Every command takes `--json` for a clean machine-readable object (also emitted -automatically when stdout is not a TTY, so pipes Just Work). Errors in `--json` -mode are `{"error": {"code", "message", "status"}}` on stdout with a nonzero -exit (an out-of-credits run reports `"code": "no_credits"` and adds -`upgrade_url`). `lenz usage` leads with the balance: - -```text -Lenz usage (Pro plan) - 5070 credits left (≈ 507 verifications · 5070 assessments) - Verify: 507 left (13 / 520 quota + 20 extra · 10 credits each · 5 at depth "low") - Ask: 5070 left (130 / 5200 quota + 200 extra · 1 credit each) - Assess: 5070 left (130 / 5200 quota + 200 extra · 1 credit each) - Extract: 4 / 1000 today (free — no credit charge) - Credits reset in 3 days (Sep 1, 2026) +pip install lenz-io +export LENZ_API_KEY=lenz_... ``` -`verify` blocks with a progress spinner; Ctrl-C prints a -`lenz verify --resume ` handle so a long run isn't lost. Key resolution -order is `--api-key` flag → `LENZ_API_KEY` → `~/.config/lenz/config.json`. - -**Scripting the lifecycle (no blocking).** `verify --detach` returns a -`task_id` immediately; poll it with `status` and read the full report with -`show` once it completes: +```python +from lenz_io import Lenz -```bash -tid=$(lenz verify "" --detach --json | jq -r .task_id) -lenz status "$tid" --json | jq -r .status # processing → completed -lenz show --json # full report once done +client = Lenz() # reads LENZ_API_KEY +row = client.assess(claim="The Great Wall of China is visible from space.").claims[0] +print(row.verdict, row.confidence) # e.g. False high (about 15-20 s, 1 credit) ``` -If the input holds several claims, `status` reports `needs_input` and lists -them; resolve it non-interactively by index (spawns one verification per pick): - -```bash -lenz verify --resume "$tid" --claim 1,3 --detach --json # → spawned task_ids -``` +From there: [the whole ladder](#quickstart--the-canonical-integration) on an +LLM answer, [a review](#review-a-draft) of a draft, or [the command-line +tool](#command-line-tool). ## Review a draft @@ -227,8 +174,12 @@ To submit without waiting, `client.review(draft)` returns a `review_id`; read it with `client.get_review(review_id)`, or only the issues with `client.get_review(review_id, view="issues")`. +A runnable version, with the errors handled: +[`examples/core/review_draft.py`](examples/core/review_draft.py). + **Credits.** 1 per claim assessed, plus 10 (5 at `depth="low"`) per deep -check; `review.credits.charged` says what the review cost. A resend with the +check, plus 1 per checked citation; `review.credits.charged` says what the +review cost. A resend with the same `Idempotency-Key` within 24 hours returns the same review; a new key is a new review. @@ -281,12 +232,84 @@ The body carries the same rows as a review's: `citations`, `citation_issues`, `citecheck_id` at once; read it with `client.get_citecheck(citecheck_id)`. `citecheck_and_wait` raises `CitecheckFailed` when the check fails and `CitecheckTimeout` after `timeout` seconds. The `citecheck.completed` and -`citecheck.failed` webhooks parse into a `CitecheckEvent`. +`citecheck.failed` webhooks parse into a `CitecheckEvent`. A runnable version: +[`examples/core/citecheck_draft.py`](examples/core/citecheck_draft.py). From the terminal: `lenz citecheck draft.md` (or `--pairs pairs.json`, a JSON list of pairs) prints the count, the key numbers and each source issue, and exits `0` clean, `1` issues found, `2` anything else. +## Command-line tool + +The same primitives from your terminal — submit, poll, and read full reports. +Ships inside this package behind the `cli` extra (quotes matter — bare brackets +are a glob in zsh): + +```bash +pipx install "lenz-io[cli]" # isolated CLI install (recommended) +pip install "lenz-io[cli]" # or into your current environment +``` + +```bash +lenz login # paste an API key (free — get one at lenz.io/api-credentials) +lenz extract "Einstein won the 1921 Nobel for relativity" # free, 1000/day +lenz extract "$(cat deck.txt)" --focus "market size" # only the claims you want +lenz extract "$(cat draft.txt)" --locate # where the text makes each claim +lenz assess "The Great Wall is visible from space" # fast verdict +lenz assess "" "" "" # one call, one verdict per claim (up to 20) +lenz verify "Water boils at 90C at sea level" # full pipeline (~90s) +lenz verify "" --depth low # shallower, faster, half the credits +lenz verify "" --json | jq .verdict # machine-readable +lenz status # non-blocking: poll a verify task's progress +lenz show # full report — sources, warnings, panel + debate (-c for concise) +lenz ask "Which source is strongest?" +lenz review draft.md # the whole draft: quick verdicts, deep checks, and up to 20 of its sources +lenz review draft.md --issues # only the issues +lenz review draft.md --max-citations 0 # claims only, no source checked +lenz review draft.md --max-assessments 0 # only the sources, no claim +lenz citecheck draft.md # the citation check on its own +lenz citecheck --pairs pairs.json # statement-source pairs, each checked as it is +lenz usage # credits left, what they buy, and when they reset +lenz config # show which key/base URL is in use +``` + +Every command takes `--json` for a clean machine-readable object (also emitted +automatically when stdout is not a TTY, so pipes Just Work). Errors in `--json` +mode are `{"error": {"code", "message", "status"}}` on stdout with a nonzero +exit (an out-of-credits run reports `"code": "no_credits"` and adds +`upgrade_url`). `lenz usage` leads with the balance: + +```text +Lenz usage (Pro plan) + 5070 credits left (≈ 507 verifications · 5070 assessments) + Verify: 507 left (13 / 520 quota + 20 extra · 10 credits each · 5 at depth "low") + Ask: 5070 left (130 / 5200 quota + 200 extra · 1 credit each) + Assess: 5070 left (130 / 5200 quota + 200 extra · 1 credit each) + Extract: 4 / 1000 today (free — no credit charge) + Credits reset in 3 days (Sep 1, 2026) +``` + +`verify` blocks with a progress spinner; Ctrl-C prints a +`lenz verify --resume ` handle so a long run isn't lost. Key resolution +order is `--api-key` flag → `LENZ_API_KEY` → `~/.config/lenz/config.json`. + +**Scripting the lifecycle (no blocking).** `verify --detach` returns a +`task_id` immediately; poll it with `status` and read the full report with +`show` once it completes: + +```bash +tid=$(lenz verify "" --detach --json | jq -r .task_id) +lenz status "$tid" --json | jq -r .status # processing → completed +lenz show --json # full report once done +``` + +If the input holds several claims, `status` reports `needs_input` and lists +them; resolve it non-interactively by index (spawns one verification per pick): + +```bash +lenz verify --resume "$tid" --claim 1,3 --detach --json # → spawned task_ids +``` + ## Quickstart — the canonical integration ```python @@ -302,6 +325,9 @@ claims = [c.claim for c in out.claims] # 2. assess — one call per 20 claims (extract finds up to 100), one row per claim, same order quick = [row for i in range(0, len(claims), 20) for row in client.assess(claims=claims[i : i + 20]).claims] for c in quick: + if c.status == "failed": # no verdict: failure.code says why, failure.hint what to send next + print("failed", c.failure.code if c.failure else "", c.claim) + continue print(c.verdict, c.confidence, c.claim) if c.rationale: print(" ", c.rationale) @@ -314,10 +340,11 @@ for r in results: if r.verification: print(r.verification.verdict, r.verification.lenz_score, r.verification.executive_summary) -# 4. ask — follow-up grounded on a verification -v = results[0].verification -reply = client.ask.send(v.verification_id, message="Which source is strongest?") -print(reply.content) +# 4. ask — follow-up grounded on a verification (when a claim was escalated) +deep = next((r.verification for r in results if r.verification is not None), None) +if deep is not None: + reply = client.ask.send(deep.verification_id, message="Which source is strongest?") + print(reply.content) ``` `assess(claims=[...])` takes up to 20 claims per call and always answers @@ -395,8 +422,8 @@ your own claims. Use webhooks for production async flows. - **`client.verify_batch(claims=[...])`** → `BatchAccepted`. Fan-out for multi-claim LLM outputs. - **`client.verify_batch_and_wait(claims=[...])`** → `list[BatchItemResult]`. Fan out a batch and poll every item to completion; one result per claim, in input order, never raises on a per-item failure. - **`client.ask.{history,send,reset}(verification_id, ...)`** → Q&A on a verification. `reply.content` uses a small markdown subset (`**bold**`, `*italic*`, `- ` or `* ` bullets, blank-line paragraphs) — render with a minimal markdown library or display verbatim. See [docs/quickstart#ask-reply-format](https://lenz.io/docs/quickstart#ask-reply-format). -- **`client.verifications.{list,get,delete,related}(...)`** → manage past verifications. All API claims are private; reference them by `verification_id`. Cache-hit on another customer's claim is transparent — you always see your own `verification_id`, never another customer's. -- **`client.library.list(...)`** → browse the public catalog (no API key needed). +- **`client.verifications.{list,iter,get,delete,related}(...)`** → manage past verifications. `iter()` walks every page lazily (`for item in client.verifications.iter(): ...`). All API claims are private; reference them by `verification_id`. Cache-hit on another customer's claim is transparent — you always see your own `verification_id`, never another customer's. +- **`client.library.list(...)`** / **`client.library.iter(...)`** → browse the public catalog (no API key needed); `iter` walks every page lazily and refuses `sort="random"`, which is not exhaustive. - **`client.usage()`** → the account's credit balance (`usage.credits`), the price list (`usage.costs` — `verify` 10, `assess` 1, `ask` 1, `extract` 0 — plus `usage.cost_options` for parameter-dependent prices such as `depth`), and per-capability projections of that one pool (`usage.verify.remaining` is how many verifications the balance still buys), plus the daily `extract` rate limit. Also reports `has_webhook_secret` — whether this key can receive signed webhook callbacks (`verify` with a `webhook_url` needs one); the secret value itself is never exposed. ## Polling without webhooks @@ -471,6 +498,18 @@ Every claim-shaped response shares these fields at top level: | `confidence` | `str` | Categorical: `"high"` \| `"medium"` \| `"low"`. | | `lenz_score` | `int \| None` | Integer 1–10 (deep verdicts and list endpoints; `assess` omits it). | +The accepted values are exported as `Literal` aliases for comparisons and +exhaustive matching: `Verdict` (the five labels and `"Error"`), `VerdictLabel` +(the five labels), `Confidence` and `Depth` (`"standard"` or `"low"`). The +fields themselves stay `str`, so a value a later API adds still reads. + +```python +from lenz_io import Verdict + +NEEDS_A_LOOK: set[Verdict] = {"False", "Mostly False", "Mixed"} +flagged = [row for row in quick if row.verdict in NEEDS_A_LOOK] +``` + ### Field names: the current names, and the deprecated 2.x ones The API's current response shape gives each field one name across every @@ -593,20 +632,27 @@ webhooks = LenzWebhooks(secret="whsec_...") # In your web handler: event = webhooks.parse(raw_body=request.body, headers=request.headers) if isinstance(event, VerificationCompleted): - vid, result = event.verification_id, event.result - # result["verdict"], result["lenz_score"], result["confidence"], ... + # event.verification is the verification as client.get_status returns it + # (a TaskStatus); the verdict is under .result, a typed Verification. + v = event.verification.result if event.verification else None + if v is not None: + print(v.verification_id, v.verdict, v.lenz_score, v.confidence) elif isinstance(event, VerificationNeedsInput): - tid, ni = event.task_id, event.needs_input - ... + options = [c.claim for c in event.claims] # pick, then client.select(event.task_id, claims=[...]) elif isinstance(event, VerificationFailed): - # event.error is WHERE the pipeline stopped; event.failure_class is WHY - # (closed set) and event.retryable tells you what to do about it. - if event.retryable: + # failure.code is WHAT stopped it; failure_class is WHY (closed set) and + # retryable tells you what to do about it. + if event.failure and event.failure.retryable: resubmit_later(event.task_id) # transient provider outage else: - log_permanent_failure(event.task_id, event.error) + log_permanent_failure(event.task_id, event.failure.code if event.failure else "") ``` +`.verification` (since 3.0) is built from either payload shape, and is `None` +only when a payload cannot be read as one. The flat `event.result` dict (and +`error`, `failure_class`, `retryable` on a failed event) are kept with their +2.x values; prefer `.verification` and `.failure`. + If you're on Python 3.10+ a `match` statement reads even cleaner — events are plain dataclasses, so structural pattern matching works. @@ -727,6 +773,8 @@ support tickets: ```python from lenz_io import ( LenzAuthError, + LenzConnectionError, + LenzNotFoundError, LenzQuotaExceededError, LenzRateLimitError, LenzUpstreamUnavailableError, @@ -766,8 +814,50 @@ except LenzUpstreamUnavailableError as exc: # charged. Waits up to 60s are already slept through by the automatic # retry ladder; reaching here means the server stated a longer one. schedule_retry_in(exc.retry_after) # typically 90-120s +except LenzNotFoundError as exc: + # HTTP 404. The id (or key) the request names finds nothing: retrying + # will not change that. A LenzError, as in 2.x. + print(exc.fix) +except LenzConnectionError as exc: + # No answer at all, after the SDK's own retries: the network, DNS, TLS, + # or (LenzRequestTimeoutError, a subclass) one request's timeout. A + # LenzAPIError, as in 2.x; exc.__cause__ is the httpx exception. + schedule_retry_in(30) ``` +**`retryable`** (since 3.0, on every error): whether sending the same request +again can succeed. `True` for a connection failure, a request timeout, a 429 +and a 5xx; `False` for any other 4xx, a version error and the client-side +errors a resend cannot fix; `None` when the SDK cannot say. A failed +verification, review or citation check carries the server's own value (`None` +when it sent none). Calls that charge send an `Idempotency-Key`, so resending +one the SDK raised for replays rather than runs twice when you pass the same +key. + +```python +from lenz_io import LenzError + +try: + client.assess(claim="...") +except LenzError as exc: + if exc.retryable: + schedule_retry_in(getattr(exc, "retry_after", None) or 30) + else: + raise +``` + +Local argument mistakes (an empty id, two exclusive arguments) raise +`ValueError`, never a `LenzError`. + +A `*_and_wait` helper that reaches its own `timeout` raises `LenzTimeoutError` +(`ReviewTimeout`, `CitecheckTimeout`): the job keeps running server-side, so +read it later by its id rather than resubmitting. A poll answered 401, 403 or +404 ends the wait at once with that error; a 5xx, a 429 or a network failure +is polled again. `ReviewFailed`, `ReviewTimeout`, `CitecheckFailed` and +`CitecheckTimeout` are also importable as `ReviewFailedError`, +`ReviewTimeoutError`, `CitecheckFailedError` and `CitecheckTimeoutError` (the +same classes). + `LenzApiVersionError` (a `LenzError`) is raised when a response names an API version other than the one this SDK reads. Every response carries the version that served it in `X-Lenz-API-Version`; lenz-io 3.x asks for `2026-10-11` and @@ -778,6 +868,15 @@ before the change) is refused rather than misread. It carries `api_version` contact support with the request id; lenz-io 2.x reads both versions. A response without the header is read as usual, and webhook payloads are never refused (they are parsed in either shape). +**Replays of requests made before the switch.** An idempotent request first +sent before lenz.io served `2026-10-11`, and replayed with the same +`Idempotency-Key` afterwards, answers with the stored reply in `2026-05-13`, +which 3.x refuses with `LenzApiVersionError`. Replays are kept for 24 hours +and replies stored since 2026-10-09 are kept in both versions, so in practice +none remain when 3.0 ships. If you meet one, finish that work with lenz-io +2.x; never change the key to get past it, which would run (and charge) the +request a second time. + ```python from lenz_io import LenzApiVersionError @@ -825,15 +924,19 @@ if status.status == "completed": ## Idempotency -`verify_and_wait` sends an auto-generated `Idempotency-Key` on every call by -default, so a network drop after submit doesn't spawn a duplicate verification -or charge a second credit. Override with `idempotency_key="..."` to pin a -specific key, or `idempotency=False` to opt out. - -`assess` does the same, and `review` always sends one (pin it with `idempotency_key=`). `ask.send` takes an `idempotency_key="..."` too, but -never generates one: re-asking the same question is a normal thing to do, and -a key you did not choose would replay the earlier answer. Pass one when your -retry means "the same question, once" — the reply, the credit and the +Every call that charges or starts work sends an auto-generated +`Idempotency-Key` by default: `verify`, `verify_and_wait`, `verify_batch`, +`verify_batch_and_wait`, `select`, `assess`, `extract` and `ask.send` (one +random key per call, reused across that call's own retries), so a network +drop after submit doesn't spawn a duplicate or charge a second time. Override +with `idempotency_key="..."` to pin a specific key (it also makes a retry +from another process replay), or `idempotency=False` to opt out. `review` and +`citecheck` always send one (pin it with `idempotency_key=`). The batch and +`ask.send` keys are new in 3.0; 2.x sent one there only when you passed it. + +The key is never derived from the request: asking the same question again on +`ask.send` is a new call, with a new key, and is asked again. Pin a key when +your retry means "the same question, once" — the reply, the credit and the conversation history are then all the first call's: ```python diff --git a/examples/core/citecheck_draft.py b/examples/core/citecheck_draft.py new file mode 100644 index 0000000..0c593ec --- /dev/null +++ b/examples/core/citecheck_draft.py @@ -0,0 +1,57 @@ +"""Check a draft's citations on their own: does each source say what the +draft says it does? + +Run: + export LENZ_API_KEY=lenz_... + python examples/core/citecheck_draft.py + +Send a draft (its links and DOIs are read from the text; keep a link as a +markdown link) or the statement-source pairs yourself. 1 credit per checked +citation; a citation that could not be checked is not charged. +""" + +from __future__ import annotations + +import os + +from lenz_io import CitationPair, Citecheck, CitecheckFailedError, CitecheckTimeoutError, Lenz + +DRAFT = ( + "Water boils at 100 degrees Celsius at sea level, according to " + "[the encyclopedia entry](https://en.wikipedia.org/wiki/Boiling_point)." +) + +PAIRS: list[CitationPair] = [ + { + "statement": "Diamond sensors can measure temperature inside a living cell.", + "doi": "10.1038/nature12373", + "cited_year": "2013", + }, +] + + +def report(label: str, check: Citecheck) -> None: + print(f"{label}: {check.outcome}") # clean | issues_found | incomplete | unchecked + for row in check.citations: + finding = row.result.finding if row.result else "pending" + print(f" {finding}: {row.cited_url or row.doi}") + for issue in check.citation_issues: # most serious first + print(f" ISSUE {issue.finding}: {issue.statement}") + if issue.snippet: + print(f" The source says: {issue.snippet}") + + +def main() -> None: + client = Lenz(api_key=os.environ.get("LENZ_API_KEY")) + try: + report("draft", client.citecheck_and_wait(DRAFT, max_citations=10)) + report("pairs", client.citecheck_and_wait(pairs=PAIRS)) + except CitecheckTimeoutError as exc: + # The check keeps running: read it later by its id, never resubmit. + print("Still running; read it later with client.get_citecheck:", exc.citecheck_id) + except CitecheckFailedError as exc: + print("The check failed:", exc.error_code, exc.hint) + + +if __name__ == "__main__": + main() diff --git a/examples/core/fastapi_webhook.py b/examples/core/fastapi_webhook.py index b647c5a..021b219 100644 --- a/examples/core/fastapi_webhook.py +++ b/examples/core/fastapi_webhook.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-decorators """Receive Lenz webhook events in a FastAPI app. Lenz POSTs HMAC-signed payloads to your ``webhook_url`` when a diff --git a/examples/core/quickstart.py b/examples/core/quickstart.py index ff03abf..5b422e6 100644 --- a/examples/core/quickstart.py +++ b/examples/core/quickstart.py @@ -1,4 +1,4 @@ -"""Lenz quickstart — the canonical four-primitive integration. +"""Lenz quickstart — the canonical integration: the four-call ladder. Run: export LENZ_API_KEY=lenz_... @@ -18,8 +18,9 @@ from __future__ import annotations import os +from typing import Any -from lenz_io import Lenz +from lenz_io import Lenz, VerifyBatchItem def main() -> None: @@ -35,24 +36,28 @@ def main() -> None: # 2. assess — one call over the extracted claims, one row per claim (sync) quick = client.assess(claims=claims).claims - for c in quick: - print(f" {c.verdict:<12} conf={c.confidence:<7} {c.claim}") - if c.failure: + for row in quick: + print(f" {row.verdict:<12} conf={row.confidence:<7} {row.claim}") + if row.status == "failed" and row.failure: # No verdict for this item — failure.code says why, failure.hint says what to send next. - print(f" {c.failure.code}: {c.failure.hint}") - if c.more_claims: + print(f" {row.failure.code}: {row.failure.hint}") + if row.more_claims: # A compound item: only its main claim was assessed. - print(f" also found (not assessed): {c.more_claims}") + print(f" also found (not assessed): {row.more_claims}") print() # 3. verify — escalate the low-confidence rows to the full multi-model panel # verify_batch_and_wait takes up to 20 claims a call: the first 20 here - doubtful = [{"claim": c.claim} for c in quick if c.status == "completed" and c.confidence == "low"][:20] + low = [row for row in quick if row.status == "completed" and row.confidence == "low"] + doubtful: list[VerifyBatchItem | dict[str, Any]] = [{"claim": row.claim} for row in low[:20]] # Fall back to the demo claim so the walkthrough always reaches steps 3 # and 4 even when every row came back confident. doubtful = doubtful or [{"claim": "Sharks don't get cancer"}] results = client.verify_batch_and_wait(claims=doubtful) - v = next(r.verification for r in results if r.verification is not None) + v = next((r.verification for r in results if r.verification is not None), None) + if v is None: + print("No verification completed:", [r.status for r in results]) + return print(f"Verdict: {v.verdict} (lenz_score {v.lenz_score}, confidence {v.confidence})") print(f"Summary: {v.executive_summary}") print() @@ -65,7 +70,7 @@ def main() -> None: reply = client.ask.send(v.verification_id, message="Which source is strongest?") print() print("Q: Which source is strongest?") - print(f"A: {reply.reply}") + print(f"A: {reply.content}") if __name__ == "__main__": diff --git a/examples/core/review_draft.py b/examples/core/review_draft.py new file mode 100644 index 0000000..5bd29ec --- /dev/null +++ b/examples/core/review_draft.py @@ -0,0 +1,66 @@ +"""Review a draft: every claim quick-checked, the doubtful ones deep-checked. + +Run: + export LENZ_API_KEY=lenz_... + python examples/core/review_draft.py + +One call pulls the claims out of the draft, gives each a quick verdict, sends +the ones that look wrong or uncertain through the deep check (up to five by +default) and returns the issues with suggested rewrites. It takes two to four +minutes; ``on_update`` shows the quick verdicts as soon as they are in. + +Credits: 1 per claim assessed, plus 10 per deep check (5 at ``depth="low"``), +plus 1 per checked citation; ``review.credits.charged`` says what it cost. +""" + +from __future__ import annotations + +import os + +from lenz_io import Lenz, LenzError, ReviewFailedError, ReviewFull, ReviewTimeoutError + +DRAFT = """ +The EU AI Act entered into force on 1 August 2024, and its obligations for +general-purpose models applied from 2 August 2025. Fines for prohibited +practices reach 7% of global annual turnover, according to +[the regulation](https://eur-lex.europa.eu/eli/reg/2024/1689/oj). +""" + + +def show_progress(review: ReviewFull) -> None: + done = sum(1 for c in review.claims if c.assessment is not None and c.assessment.status != "pending") + print(f" {review.status}: {done}/{len(review.claims)} claims have a quick verdict") + + +def main() -> None: + client = Lenz(api_key=os.environ.get("LENZ_API_KEY")) + try: + review = client.review_and_wait( + DRAFT, + max_citations=5, # also check the draft's first 5 sources (1 credit each) + suggest_edits=True, # the smallest edits that make the draft say what each rewrite says + on_update=show_progress, + ) + except ReviewTimeoutError as exc: + # The review keeps running: read it later by its id, never resubmit. + print("Still running; read it later with client.get_review:", exc.review_id) + return + except ReviewFailedError as exc: + print("The review failed:", exc.error_code, exc.hint) + return + except LenzError as exc: + print("Could not run the review:", exc.message, "(retryable)" if exc.retryable else "") + return + + print(f"\nOutcome: {review.outcome}") # clean | issues_found | incomplete | unchecked + for issue in review.issues: + print(f"- {issue.verdict} ({issue.confidence}): {issue.claim}") + if issue.suggested_rewrite: + print(f" Suggested rewrite: {issue.suggested_rewrite}") + for citation in review.citation_issues: # most serious first + print(f"- Source issue {citation.finding}: {citation.cited_url or citation.doi}") + print(f"Credits charged: {review.credits.charged if review.credits else 'unknown'}") + + +if __name__ == "__main__": + main() diff --git a/examples/core/verify_llm_output.py b/examples/core/verify_llm_output.py index 604ca3d..99db5fe 100644 --- a/examples/core/verify_llm_output.py +++ b/examples/core/verify_llm_output.py @@ -15,8 +15,9 @@ from __future__ import annotations import os +from typing import Any -from lenz_io import Lenz +from lenz_io import Lenz, VerifyBatchItem LLM_OUTPUT = """ The Eiffel Tower was completed in 1889 and stands 330 meters tall. @@ -43,7 +44,7 @@ def main() -> None: print(f"Assessed {len(quick)} claims:\n") for c in quick: print(f" {c.verdict:<12} conf={c.confidence:<7} {c.claim}") - if c.failure: + if c.status == "failed" and c.failure: print(f" {c.failure.code}: {c.failure.hint}") elif c.more_claims: print(f" also found (not assessed): {c.more_claims}") @@ -54,7 +55,8 @@ def main() -> None: # claim that already has a deep verification surfaces immediately # via ``verification_url`` and you can skip the escalation. # verify_batch_and_wait takes up to 20 claims a call: the first 20 here - doubtful = [{"claim": c.claim} for c in quick if c.status == "completed" and c.confidence == "low"][:20] + low = [c for c in quick if c.status == "completed" and c.confidence == "low"] + doubtful: list[VerifyBatchItem | dict[str, Any]] = [{"claim": c.claim} for c in low[:20]] print(f"Escalating {len(doubtful)} low-confidence claims to full verification:\n") results = client.verify_batch_and_wait(claims=doubtful, timeout=180) if doubtful else [] for r in results: diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit index b93799c..64f3caf 100755 --- a/scripts/hooks/pre-commit +++ b/scripts/hooks/pre-commit @@ -9,7 +9,7 @@ # Runs (~10s total on a warm cache): # 1. ruff check — lint # 2. ruff format — formatting -# 3. mypy — strict type-check on src/lenz_io +# 3. mypy — strict type-check on src/lenz_io and examples/ # 4. pytest — unit suite (skips live-API smoke; those run in CI / on release) # # Skip on a specific commit with `git commit --no-verify`. Don't habituate. @@ -43,6 +43,7 @@ done_ok "ruff format" step "mypy (strict)" uv run mypy src/lenz_io || fail "mypy" +uv run mypy examples || fail "mypy (examples)" done_ok "mypy" step "pytest (unit; skips live-API smoke)" diff --git a/src/lenz_io/__init__.py b/src/lenz_io/__init__.py index af3f3ab..eb820c0 100644 --- a/src/lenz_io/__init__.py +++ b/src/lenz_io/__init__.py @@ -2,9 +2,16 @@ pip install lenz-io -The fact-check API for AI products. Four primitives form a research-depth -ladder — find claims, judge them fast, prove them deep, follow up — and a -fifth call runs the ladder on a whole draft: +The fact-check API for AI products, in six calls: four form a research-depth +ladder (find claims, judge them fast, prove them deep, follow up), ``review`` +runs the ladder on a whole draft, and ``citecheck`` checks a draft's sources +on its own: + + from lenz_io import Lenz + client = Lenz() # reads LENZ_API_KEY + + row = client.assess(claim="The Great Wall of China is visible from space.").claims[0] + print(row.verdict, row.confidence) # /review — every claim quick-checked, the doubtful ones deep-checked (2-4 min) review = client.review_and_wait(text=draft) @@ -30,9 +37,10 @@ doubtful = [{"claim": c.claim} for c in quick if c.status == "completed" and c.confidence == "low"][:20] results = client.verify_batch_and_wait(claims=doubtful) if doubtful else [] - # 4. /ask — follow-up questions grounded on a verification - deep = results[0].verification - reply = client.ask.send(deep.verification_id, message="Which source is strongest?") + # 4. /ask — follow-up questions grounded on a verification (when one was escalated) + deep = next((r.verification for r in results if r.verification is not None), None) + if deep is not None: + reply = client.ask.send(deep.verification_id, message="Which source is strongest?") See https://lenz.io/api/v1/docs/ for the full API reference. """ diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 008f789..5ace21f 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -18,12 +18,13 @@ byte-identical English path. The empty-default-then-omit convention exists precisely to avoid that. -Shape (four-primitive ladder + the supporting reads): +Shape (six calls: the four-call ladder, ``review`` and ``citecheck``, plus +the supporting reads): from lenz_io import Lenz client = Lenz(api_key="lenz_...") - # Marquee verbs — top-level (the four-primitive ladder) + # Marquee verbs — top-level (the four-call ladder) out = client.extract(text="...") # find claims in a document r = client.assess(claims=[...]) # one fast verdict per claim, up to 20 r = client.assess(claim="...") # ...or a single claim, ~15s @@ -305,8 +306,9 @@ class VerifyBatchItem(TypedDict, total=False): # 'private' (default) or 'unlisted' (link-readable, never listed). # Per-item value overrides the batch-wide ``visibility`` default. visibility: str - # 'standard' (default) or 'low' (shallower check — fewer sources, - # faster). Per-item value overrides the batch-wide ``depth`` default. + # 'standard' (default, 10 credits) or 'low' (shallower check — fewer + # sources, faster, 5 credits). Per-item value overrides the batch-wide + # ``depth`` default. depth: str @@ -689,8 +691,8 @@ def verify( never surfaced in the Library or search). Omit for private. ``depth`` (optional): ``'standard'`` (default) or ``'low'``. ``'low'`` - runs a shallower check — fewer sources, faster. Same models, same - quota cost. The completed ``Verification.depth`` echoes the depth the + runs a shallower check — fewer sources, faster, same models — for half + the credits (5 instead of 10). The completed ``Verification.depth`` echoes the depth the verdict was actually produced with, which can be ``'standard'`` for a ``'low'`` request served from cache. @@ -741,8 +743,9 @@ def verify_batch( to override the batch-wide value. ``depth`` (optional): batch-wide default, ``'standard'`` or ``'low'`` - (shallower check — fewer sources, faster). Each item dict may set its - own ``depth`` key to override the batch-wide value. + (shallower check — fewer sources, faster, 5 credits instead of 10). + Each item dict may set its own ``depth`` key to override the + batch-wide value. ``idempotency`` (default ``True``): send an ``Idempotency-Key`` for the whole batch, so a retry after a network drop returns the tasks the From 37a303e664f69abf3767cc8824f13770fba1ca83 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:14:53 +0300 Subject: [PATCH 30/67] fix: match the Node SDK on the 404 fix text, retryable and the iterator refusal The 404 fix reads "Check the id or key the call names: nothing with it is visible to this credential. Retrying will not help." retryable is None when there was no HTTP status (a missing key, a wait timeout, a needs-input pause, a webhook signature), and a top-level boolean retryable wins after the failure block's. library.iter's sort="random" refusal uses the Node words. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 17 +++++++++-------- README.md | 4 ++-- src/lenz_io/client.py | 5 +---- src/lenz_io/errors.py | 44 ++++++++++++++++++------------------------- tests/test_errors.py | 20 ++++++++++++++------ tests/test_parity.py | 4 +++- 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de6a071..bf546a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,8 +124,9 @@ Deprecated). (a `LenzConnectionError`), both `LenzAPIError`s, with the same message and the `httpx` exception as `__cause__`. - **A 404 raises `LenzNotFoundError`** (a `LenzError`, as before) and its - `fix` reads "Check the id the request names and the API key it was sent - with: retrying will not find it." (2.x advised retrying). The message, the + `fix` reads "Check the id or key the call names: nothing with it is + visible to this credential. Retrying will not help." (2.x advised + retrying). The message, the other fields and every other error's text are unchanged. ### Added @@ -134,12 +135,12 @@ Deprecated). `LenzRequestTimeoutError` (see Changed). - **`retryable` on every error**, set when the error is built: whether sending the same request again can succeed. `True` for a connection failure, a - request timeout, a 429 and a 5xx; `False` for any other 4xx, a - `LenzApiVersionError` and the client-side errors a resend cannot fix (a - missing key, a `*_and_wait` timeout, a needs-input pause, a bad webhook - signature); `None` when the SDK cannot say. A failed verification, review - or citation check keeps the server's value (`None` when it sent none), and a - boolean `retryable` in a response's `failure` block always wins. A + request timeout, a 429 and a 5xx; `False` for any other 4xx and a + `LenzApiVersionError`; `None` when there was no HTTP status (a missing key, + a `*_and_wait` timeout, a needs-input pause, a bad webhook signature). A + failed verification, review or citation check keeps the server's value + (`None` when it sent none), and a boolean `retryable` in a response's + `failure` block, else at its top level, always wins. A `retryable=` passed to an error's constructor wins too. - `ReviewFailedError`, `ReviewTimeoutError`, `CitecheckFailedError` and `CitecheckTimeoutError`: the job errors under the names the Node SDK uses diff --git a/README.md b/README.md index 33da51d..5093af6 100644 --- a/README.md +++ b/README.md @@ -827,8 +827,8 @@ except LenzConnectionError as exc: **`retryable`** (since 3.0, on every error): whether sending the same request again can succeed. `True` for a connection failure, a request timeout, a 429 -and a 5xx; `False` for any other 4xx, a version error and the client-side -errors a resend cannot fix; `None` when the SDK cannot say. A failed +and a 5xx; `False` for any other 4xx and a version error; `None` when there was no HTTP +status (a missing key, a `*_and_wait` timeout). A failed verification, review or citation check carries the server's own value (`None` when it sent none). Calls that charge send an `Idempotency-Key`, so resending one the SDK raised for replays rather than runs twice when you pass the same diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 5ace21f..0db14dc 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -567,10 +567,7 @@ def iter( avoids repeats (use ``list(sort="random")`` for a sample). Since 3.0. """ if sort == "random": - raise ValueError( - "library.iter() does not take sort='random': a random order is not exhaustive. " - "Use library.list(sort='random') for a sample." - ) + raise ValueError('iter cannot walk sort="random" (each page is a fresh sample); call library.list instead.') return _walk( lambda n: self.list( page=n, sort=sort, search=search, domain=domain, entity=entity, curated=curated, verdict=verdict diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 7a1586f..429a2b8 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -51,8 +51,9 @@ class LenzError(Exception): * ``body`` — parsed JSON response body if available * ``retryable`` — whether sending the same request again can succeed: ``True`` for a network failure, a transport timeout, a 429 and a 5xx; - ``False`` for any other 4xx, a version error and the client-side - errors a resend cannot fix; ``None`` when the SDK cannot say. A failed + ``False`` for any other 4xx and a version error; ``None`` when there + was no HTTP status (a missing key, a ``*_and_wait`` timeout, a + needs-input pause, a bad webhook signature). A failed verification, review or citation check carries the server's value (``None`` when it sent none), and a boolean ``retryable`` in the response's ``failure`` block always wins. Since 3.0; set on every @@ -124,10 +125,6 @@ class LenzAuthError(LenzError): and "top up your account" are different actions. """ - def _derived_retryable(self) -> bool | None: - # Also the client-side "API key required" (status 0). - return False - class LenzQuotaExceededError(LenzError): """402 — you're out of balance, or your plan doesn't cover this call. @@ -316,16 +313,13 @@ class LenzTimeoutError(LenzError): """``verify_and_wait`` exceeded the configured timeout. ``task_id`` is set so callers can resume via ``client.get_status(task_id)``. - The job keeps running server-side: read it later rather than resubmit, so - ``retryable`` is ``False``. A single HTTP request that timed out is + The job keeps running server-side: read it later by its id rather than + resubmit (``retryable`` is ``None``: no request failed). A single HTTP request that timed out is :class:`LenzRequestTimeoutError` instead. """ task_id: str = "" - def _derived_retryable(self) -> bool | None: - return False - class LenzNeedsInputError(LenzError): """``verify_and_wait`` paused because the pipeline needs caller input. @@ -341,10 +335,6 @@ class LenzNeedsInputError(LenzError): hint: str = "" payload: dict[str, Any] = {} # noqa: RUF012 — overridden per-instance - def _derived_retryable(self) -> bool | None: - # A resend pauses the same way: resolve it with ``select``. - return False - class LenzPipelineError(LenzError): """A verification run ended in a terminal ``failed`` state. @@ -483,9 +473,6 @@ class LenzWebhookSignatureError(LenzError): ``X-Lenz-Signature`` header, replay window exceeded, malformed body. """ - def _derived_retryable(self) -> bool | None: - return False - class LenzApiVersionError(LenzError): """A response named an API version this SDK does not read. @@ -971,14 +958,17 @@ def _read(key: str, block_key: str) -> Any: else: err.retry_after = 0 - # A boolean ``retryable`` in the body's failure block is the server's own - # word and wins over the status. (A failed verification's 409 read it - # above, with the flat field first.) + # A boolean ``retryable`` in the body's failure block, else at its top + # level, is the server's own word and wins over the status. (A failed + # verification's 409 read it above.) if not isinstance(err, LenzPipelineError): - for block in (raw.get("failure"), parsed.get("failure")): - if isinstance(block, dict) and isinstance(block.get("retryable"), bool): - err.retryable = block["retryable"] - break + candidates = [ + block.get("retryable") for block in (raw.get("failure"), parsed.get("failure")) if isinstance(block, dict) + ] + candidates += [raw.get("retryable"), parsed.get("retryable")] + stated = next((c for c in candidates if isinstance(c, bool)), None) + if stated is not None: + err.retryable = stated return err @@ -1013,7 +1003,9 @@ def _fix_hint_for(status_code: int) -> str: 401: "Your credential is missing, invalid or expired. Check the key you passed, or get a new one at https://lenz.io/api-credentials.", 403: "This key doesn't have access to that resource.", 402: "Top up or upgrade at https://lenz.io/plans, or wait for the period reset.", - 404: "Check the id the request names and the API key it was sent with: retrying will not find it.", + 404: ( + "Check the id or key the call names: nothing with it is visible to this credential. Retrying will not help." + ), 422: "Check the request body against the OpenAPI spec.", 429: "Wait Retry-After seconds and retry.", }.get(status_code, "Retry; if the error persists, file an issue with the Request ID.") diff --git a/tests/test_errors.py b/tests/test_errors.py index 459a565..5f235c8 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -479,7 +479,9 @@ def test_errors_all_lists_every_public_error_class_and_constant(): # ── 3.0: not found, connection failures, ``retryable`` ───────────────────── -NOT_FOUND_FIX = "Check the id the request names and the API key it was sent with: retrying will not find it." +NOT_FOUND_FIX = ( + "Check the id or key the call names: nothing with it is visible to this credential. Retrying will not help." +) class TestNotFound: @@ -588,6 +590,11 @@ def test_a_boolean_in_the_failure_block_wins(self, status, flag): e = map_response_to_error(status, _body({"detail": "x", "failure": {"retryable": flag}}), {}) assert e.retryable is flag + def test_a_top_level_boolean_wins_after_the_failure_block(self): + assert map_response_to_error(503, _body({"retryable": False}), {}).retryable is False + both = {"retryable": True, "failure": {"retryable": False}} + assert map_response_to_error(400, _body(both), {}).retryable is False + def test_a_non_boolean_in_the_failure_block_is_ignored(self): e = map_response_to_error(503, _body({"failure": {"retryable": "yes"}}), {}) assert e.retryable is True @@ -615,11 +622,12 @@ def test_client_side_errors(self): assert LenzError().retryable is None assert LenzPipelineError().retryable is None assert ReviewFailed().retryable is None - assert LenzAuthError(message="API key required").retryable is False - assert LenzTimeoutError().retryable is False - assert ReviewTimeout().retryable is False - assert LenzNeedsInputError().retryable is False - assert LenzWebhookSignatureError().retryable is False + # No HTTP status: unknown. + assert LenzAuthError(message="API key required").retryable is None + assert LenzTimeoutError().retryable is None + assert ReviewTimeout().retryable is None + assert LenzNeedsInputError().retryable is None + assert LenzWebhookSignatureError().retryable is None assert LenzAPIError().retryable is True assert LenzRateLimitError().retryable is True diff --git a/tests/test_parity.py b/tests/test_parity.py index efce38b..46c6992 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -114,7 +114,9 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, #: The fix line 3.0 gives a 404 (CHANGELOG "Changed"): 2.x said to retry. -_NOT_FOUND_FIX = "Check the id the request names and the API key it was sent with: retrying will not find it." +_NOT_FOUND_FIX = ( + "Check the id or key the call names: nothing with it is visible to this credential. Retrying will not help." +) def _intended(path: str, old: Any, new: Any, legacy: dict[str, Any]) -> str | None: From 377eeff496d4f39b858de9a9b831312603ce2b74 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:30:11 +0300 Subject: [PATCH 31/67] fix: retryable is true on a 409 idempotency_conflict or verification_not_ready, unknown without a status A 409 that says "not yet" is worth resending (with the same key); the body's code is read as sent, so the 2.x attributes keep their empty code. A LenzAPIError or LenzRateLimitError with no status reads None, as in the Node SDK; the connection classes stay True. Declares LenzError.idempotency_key (None by default), wired in the next commit. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/errors.py | 52 ++++++++++++++++++++++++++++++++++--------- tests/test_errors.py | 29 ++++++++++++++++++++---- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 429a2b8..d592754 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -50,10 +50,12 @@ class LenzError(Exception): server sent none. Branch on this rather than on message text. * ``body`` — parsed JSON response body if available * ``retryable`` — whether sending the same request again can succeed: - ``True`` for a network failure, a transport timeout, a 429 and a 5xx; - ``False`` for any other 4xx and a version error; ``None`` when there - was no HTTP status (a missing key, a ``*_and_wait`` timeout, a - needs-input pause, a bad webhook signature). A failed + ``True`` for a network failure, a transport timeout, a 429, a 5xx and + a 409 ``idempotency_conflict`` (the first request with that key is + still running) or ``verification_not_ready``; ``False`` for any other + 4xx and a version error; ``None`` when there was no HTTP status (a + missing key, a ``*_and_wait`` timeout, a needs-input pause, a bad + webhook signature). A failed verification, review or citation check carries the server's value (``None`` when it sent none), and a boolean ``retryable`` in the response's ``failure`` block always wins. Since 3.0; set on every @@ -61,6 +63,12 @@ class LenzError(Exception): """ retryable: bool | None + #: The ``Idempotency-Key`` the failed call sent, or ``None`` when it sent + #: none. To resend safely, pass it back (``idempotency_key=exc.idempotency_key``): + #: the server then replays the first answer, or reports the first request + #: still running, instead of running it twice. A plain new call sends a + #: new key, and can run (and charge) the work a second time. Since 3.0. + idempotency_key: str | None = None def __init__( self, @@ -97,6 +105,8 @@ def _derived_retryable(self) -> bool | None: status = self.status_code if status == 429 or 500 <= status < 600: return True + if status == 409 and _resendable_409(self): + return True if 400 <= status < 500: return False return None @@ -114,6 +124,18 @@ def __str__(self) -> str: # pragma: no cover - trivial return "\n".join(lines) +#: 409 codes that mean "not yet": the same request, sent again later (with the +#: same ``Idempotency-Key``), can succeed. +_RESENDABLE_409_CODES = ("idempotency_conflict", "verification_not_ready") + + +def _resendable_409(err: LenzError) -> bool: + """Whether a 409 says "not yet". Reads the body as sent too: the 2.x + attributes leave ``code`` empty on endpoints whose original body had none.""" + sent = err.body.get("code") if isinstance(err.body, dict) else None + return err.code in _RESENDABLE_409_CODES or sent in _RESENDABLE_409_CODES + + class LenzAuthError(LenzError): """401 / 403 — the credential is missing, invalid, expired, or revoked. @@ -248,9 +270,6 @@ class LenzRateLimitError(LenzError): #: know a paid plan raises it. upgrade_url: str = "" - def _derived_retryable(self) -> bool | None: - return True - class LenzAPIError(LenzError): """500 / 502 / 503 / 504 / catch-all for unexpected server errors. @@ -264,7 +283,9 @@ class LenzAPIError(LenzError): retry_after: int | None = None def _derived_retryable(self) -> bool | None: - return True + # Any status this class is raised for is a 5xx; without one (built by + # hand, or a request that failed with no diagnostic) it is unknown. + return None if self.status_code == 0 else True class LenzConnectionError(LenzAPIError): @@ -275,10 +296,15 @@ class LenzConnectionError(LenzAPIError): existing ``except LenzAPIError`` keeps catching it. ``__cause__`` is the underlying ``httpx`` exception and ``status_code`` is 0. ``retryable`` is ``True``: the same request can succeed once the network is back (calls - that charge send an ``Idempotency-Key``, so a resend replays rather than - runs twice). + that charge send an ``Idempotency-Key``). Resend with the same key: pass + ``idempotency_key=exc.idempotency_key`` back, and the server replays the + first answer if the first request did arrive. A plain new call sends a new + key, and can run (and charge) the work twice. """ + def _derived_retryable(self) -> bool | None: + return True + class LenzRequestTimeoutError(LenzConnectionError): """One HTTP request got no answer within its timeout (``Lenz(timeout=)`` @@ -288,6 +314,12 @@ class LenzRequestTimeoutError(LenzConnectionError): its own deadline while the job keeps running. A subclass of :class:`LenzConnectionError` and so of :class:`LenzAPIError`, which is what 2.x raised. + + The request may have reached the server and be running. Resend only with + the same key (``idempotency_key=exc.idempotency_key``): the server then + replays the first answer (or answers 409 ``idempotency_conflict`` while it + still runs) instead of running it again. A plain new call sends a new key, + and can run (and charge) the work twice. """ diff --git a/tests/test_errors.py b/tests/test_errors.py index 5f235c8..f2cf989 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -569,7 +569,8 @@ class TestRetryable: (402, {"code": "no_credits"}, False), (403, {}, False), (404, {}, False), - (409, {"code": "idempotency_conflict"}, False), + (409, {"code": "idempotency_conflict"}, True), + (409, {"code": "select_not_pending"}, False), (410, {"code": "purged"}, False), (422, {}, False), (429, {}, True), @@ -579,12 +580,27 @@ class TestRetryable: (503, {}, True), (503, {"code": "upstream_unavailable"}, True), (503, {"code": "capacity"}, True), - (409, {"code": "verification_not_ready", "status": "processing"}, False), + (409, {"code": "verification_not_ready", "status": "processing"}, True), ], ) def test_derived_from_the_response(self, status, body, expected): assert map_response_to_error(status, _body(body), {}).retryable is expected + @pytest.mark.parametrize( + ("endpoint", "code"), + [ + (("POST", "/verify"), "idempotency_conflict"), + (("POST", "/ask/v1"), "idempotency_conflict"), + (("POST", "/ask/v1"), "verification_not_ready"), + (("GET", "/verifications/v1"), "verification_not_ready"), + ], + ) + def test_a_409_worth_resending_is_retryable_whatever_code_the_2x_body_keeps(self, endpoint, code): + # The 2.x body has no ``code`` on some endpoints (``exc.code`` stays + # ""), but the answer is still worth sending again (with the same key). + e = map_response_to_error(409, _body({"detail": "x", "code": code}), {}, endpoint=endpoint) + assert e.retryable is True + @pytest.mark.parametrize(("status", "flag"), [(503, False), (400, True), (500, False), (422, True)]) def test_a_boolean_in_the_failure_block_wins(self, status, flag): e = map_response_to_error(status, _body({"detail": "x", "failure": {"retryable": flag}}), {}) @@ -628,8 +644,13 @@ def test_client_side_errors(self): assert ReviewTimeout().retryable is None assert LenzNeedsInputError().retryable is None assert LenzWebhookSignatureError().retryable is None - assert LenzAPIError().retryable is True - assert LenzRateLimitError().retryable is True + # No status and no connection failure: unknown, as in the Node SDK. + assert LenzAPIError().retryable is None + assert LenzRateLimitError().retryable is None + from lenz_io import LenzConnectionError, LenzRequestTimeoutError + + assert LenzConnectionError().retryable is True + assert LenzRequestTimeoutError().retryable is True def test_a_value_passed_in_wins(self): assert LenzPipelineError(retryable=True).retryable is True From 91736869f252643396f390663f3b0d38e29bfc69 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:30:39 +0300 Subject: [PATCH 32/67] feat: a 409 idempotency_conflict is sent again with the same key, and every error carries the key A call that sent an Idempotency-Key and meets the first request still running sends the same key and body again, after the stated wait (capped) or the usual backoff, inside its retry budget; still conflicting, it raises the 2.x error with retryable=True. A review or citation-check conflict that names the job still returns it at once. Every LenzError a keyed call raises (its wait included) carries exc.idempotency_key. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 194 +++++++++++++++++------- tests/test_client.py | 31 ++-- tests/test_idempotent_resend.py | 254 ++++++++++++++++++++++++++++++++ 3 files changed, 414 insertions(+), 65 deletions(-) create mode 100644 tests/test_idempotent_resend.py diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 0db14dc..7cc27a7 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -84,6 +84,7 @@ import time import uuid from collections.abc import Callable, Iterator +from contextlib import contextmanager from typing import Any, Literal, TypedDict, TypeVar, overload import httpx @@ -601,6 +602,31 @@ def _call_key(idempotency_key: str | None, idempotency: bool) -> str | None: return uuid.uuid4().hex if idempotency else None +@contextmanager +def _carrying_key(key: str | None) -> Iterator[None]: + """Put ``key`` on any ``LenzError`` raised inside (``exc.idempotency_key``) + that does not carry one yet: every error of a call that sent a key says + which, so a resend can reuse it.""" + try: + yield + except LenzError as exc: + if key and exc.idempotency_key is None: + exc.idempotency_key = key + raise + + +def _names_the_job(field: str) -> Callable[[Any], bool]: + """Whether a 409 ``idempotency_conflict`` body names the job the first + request created (``review_id`` / ``citecheck_id``): that answer settles the + call, so it is not sent again.""" + + def check(body: Any) -> bool: + value = body.get(field) if isinstance(body, dict) else None + return isinstance(value, str) and bool(value) + + return check + + class Lenz: """Top-level client. @@ -1026,18 +1052,19 @@ def verify_and_wait( ``timeout`` defaults to ``WAIT_TIMEOUT`` (300s). """ key = _call_key(idempotency_key, idempotency) - accepted = self._verify_submit( - claim=claim, - text=text, - source_url=source_url, - webhook_url=webhook_url, - language=language, - visibility=visibility, - depth=depth, - idempotency_key=key, - ) - logger.info("Submitted task: %s", accepted.task_id) - return self.wait(accepted, timeout=timeout, on_progress=on_progress) + with _carrying_key(key): + accepted = self._verify_submit( + claim=claim, + text=text, + source_url=source_url, + webhook_url=webhook_url, + language=language, + visibility=visibility, + depth=depth, + idempotency_key=key, + ) + logger.info("Submitted task: %s", accepted.task_id) + return self.wait(accepted, timeout=timeout, on_progress=on_progress) def wait( self, @@ -1112,16 +1139,18 @@ def verify_batch_and_wait( ``idempotency`` / ``idempotency_key``: as on ``verify_batch``. """ - accepted = self._verify_batch( - claims=claims, - webhook_url=webhook_url, - language=language, - visibility=visibility, - depth=depth, - idempotency_key=_call_key(idempotency_key, idempotency), - ) - ids = [it.task_id for it in accepted.items if it.task_id] - terminal, timed_out, stopped = self._poll_to_terminal(ids, timeout, on_progress) + key = _call_key(idempotency_key, idempotency) + with _carrying_key(key): + accepted = self._verify_batch( + claims=claims, + webhook_url=webhook_url, + language=language, + visibility=visibility, + depth=depth, + idempotency_key=key, + ) + ids = [it.task_id for it in accepted.items if it.task_id] + terminal, timed_out, stopped = self._poll_to_terminal(ids, timeout, on_progress) results: list[BatchItemResult] = [] for it in accepted.items: # preserve input order @@ -1260,7 +1289,9 @@ def review( payload["escalate"] = escalate headers = {"Idempotency-Key": idempotency_key or uuid.uuid4().hex} try: - body = self._request("POST", "/review", json=payload, headers=headers) + body = self._request( + "POST", "/review", json=payload, headers=headers, conflict_settles=_names_the_job("review_id") + ) except LenzError as exc: # A retried submit (same key) that lands while the first attempt's # review is still being created answers 409 with that review's id: @@ -1336,22 +1367,25 @@ def review_and_wait( ``timeout`` seconds pass first: the review keeps running, and the error carries its ``review_id`` and the last body read. """ - started = self.review( - text, - verdicts=verdicts, - confidence=confidence, - max_assessments=max_assessments, - max_verifications=max_verifications, - depth=depth, - max_citations=max_citations, - suggest_edits=suggest_edits, - language=language, - webhook_url=webhook_url, - visibility=visibility, - idempotency_key=idempotency_key, - ) - logger.info("Submitted review: %s", started.review_id) - return self._wait_review(started.review_id, timeout=timeout, on_update=on_update) + # The key is minted here (as ``review`` would) so the wait's errors carry it too. + key = idempotency_key or uuid.uuid4().hex + with _carrying_key(key): + started = self.review( + text, + verdicts=verdicts, + confidence=confidence, + max_assessments=max_assessments, + max_verifications=max_verifications, + depth=depth, + max_citations=max_citations, + suggest_edits=suggest_edits, + language=language, + webhook_url=webhook_url, + visibility=visibility, + idempotency_key=key, + ) + logger.info("Submitted review: %s", started.review_id) + return self._wait_review(started.review_id, timeout=timeout, on_update=on_update) # ── /citecheck: the citation check on its own ── @@ -1403,7 +1437,9 @@ def citecheck( payload["webhook_url"] = webhook_url headers = {"Idempotency-Key": idempotency_key or uuid.uuid4().hex} try: - body = self._request("POST", "/citecheck", json=payload, headers=headers) + body = self._request( + "POST", "/citecheck", json=payload, headers=headers, conflict_settles=_names_the_job("citecheck_id") + ) except LenzError as exc: # A retried submit (same key) that lands while the first attempt's # check is still being created answers 409 naming that check: it @@ -1446,16 +1482,19 @@ def citecheck_and_wait( first: the check keeps running, and the error carries its ``citecheck_id`` and the last body read. """ - started = self.citecheck( - text, - pairs=pairs, - max_citations=max_citations, - language=language, - webhook_url=webhook_url, - idempotency_key=idempotency_key, - ) - logger.info("Submitted citation check: %s", started.citecheck_id) - return self._wait_citecheck(started.citecheck_id, timeout=timeout, on_update=on_update) + # The key is minted here (as ``citecheck`` would) so the wait's errors carry it too. + key = idempotency_key or uuid.uuid4().hex + with _carrying_key(key): + started = self.citecheck( + text, + pairs=pairs, + max_citations=max_citations, + language=language, + webhook_url=webhook_url, + idempotency_key=key, + ) + logger.info("Submitted citation check: %s", started.citecheck_id) + return self._wait_citecheck(started.citecheck_id, timeout=timeout, on_update=on_update) def _wait_citecheck( self, @@ -1922,6 +1961,38 @@ def _request( auth_optional: bool = False, timeout: float | None = None, max_retries: int | None = None, + conflict_settles: Callable[[Any], bool] | None = None, + ) -> dict[str, Any]: + """One API call, with the retry ladder. Every ``LenzError`` it raises + carries the ``Idempotency-Key`` it sent (``exc.idempotency_key``).""" + key = (headers or {}).get("Idempotency-Key") or None + with _carrying_key(key): + return self._send( + method, + path, + json=json, + params=params, + headers=headers, + auth_required=auth_required, + auth_optional=auth_optional, + timeout=timeout, + max_retries=max_retries, + conflict_settles=conflict_settles, + ) + + def _send( + self, + method: str, + path: str, + *, + json: dict[str, Any] | None, + params: dict[str, Any] | None, + headers: dict[str, str] | None, + auth_required: bool, + auth_optional: bool, + timeout: float | None, + max_retries: int | None, + conflict_settles: Callable[[Any], bool] | None, ) -> dict[str, Any]: # ``max_retries`` overrides the client's retry count for this request # only (the review wait polls with 0 and does its own pacing). @@ -1980,6 +2051,22 @@ def _request( if response.status_code < 400: return response.json() if response.content else {} + # A 409 ``idempotency_conflict``: the first request with this key is + # still running. Send the SAME key and body again after the stated + # wait (capped) or the backoff, inside this call's retry budget; a + # new key would run the work twice. A conflict that names the job + # the first request created settles the call (``conflict_settles``). + if ( + attempt < retries + and response.status_code == 409 + and req_headers.get("Idempotency-Key") + and _body_error_code(response) == "idempotency_conflict" + and not (conflict_settles is not None and conflict_settles(_json_or_none(response))) + ): + stated = _stated_retry_after(response) + time.sleep(stated if stated is not None and stated <= MAX_RETRY_AFTER_SLEEP else _retry_sleep(attempt)) + continue + # Error path. Retry on 5xx and 429; otherwise raise immediately. # # A stated wait is honored only up to MAX_RETRY_AFTER_SLEEP. Past @@ -2158,6 +2245,13 @@ def _stated_retry_after(response: httpx.Response) -> int | None: return None +def _json_or_none(response: httpx.Response) -> Any: + try: + return response.json() + except Exception: + return None + + def _body_error_code(response: httpx.Response) -> str: """The server's machine-readable ``code`` from the body, or ``""``. diff --git a/tests/test_client.py b/tests/test_client.py index 70d51c8..b5baa22 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -924,32 +924,33 @@ def test_a_retry_after_a_lost_reply_gets_the_replayed_answer(self, client, path, @pytest.mark.parametrize(("path", "call", "body"), _NEW_AUTO_KEY_CALLS) def test_an_in_flight_409_is_never_passed_with_a_second_key(self, client, path, call, body, monkeypatch): # The retry lands while the first attempt still runs: the server - # answers 409 ``idempotency_conflict``. The SDK raises it, as it does - # for ``verify``; it never mints a new key to get past it, which would - # run (and charge) the request twice. + # answers 409 ``idempotency_conflict``. The SDK sends the SAME key and + # body again (as ``verify`` does) until the first call's answer comes + # back; it never mints a new key to get past it, which would run (and + # charge) the request twice. monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: None) conflict = {"detail": "A request with this key is still running.", "code": "idempotency_conflict"} with respx.mock(base_url=DEFAULT_BASE) as r: route = r.post(path) - route.side_effect = [httpx.ReadTimeout("slow"), httpx.Response(409, json=conflict)] - with pytest.raises(LenzError) as ei: - call(client) - assert ei.value.status_code == 409 - # ``code`` keeps its 2.x value (none, on this 409); the body has it. - assert ei.value.body == conflict - assert len(route.calls) == 2 + route.side_effect = [ + httpx.ReadTimeout("slow"), + httpx.Response(409, json=conflict), + httpx.Response(200, json=body), + ] + assert call(client) is not None + assert len(route.calls) == 3 assert len({c.request.headers["Idempotency-Key"] for c in route.calls}) == 1 + assert len({c.request.content for c in route.calls}) == 1 @pytest.mark.parametrize(("path", "call", "body"), _NEW_AUTO_KEY_CALLS) - def test_a_pinned_key_answers_409_then_the_result(self, client, path, call, body): - # With a key of its own the caller resends the same request after the - # 409 and gets the first call's answer. + def test_a_pinned_key_answers_409_then_the_result(self, client, path, call, body, monkeypatch): + # The call itself sends the caller's key again after the 409 and gets + # the first call's answer. + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: None) conflict = {"detail": "A request with this key is still running.", "code": "idempotency_conflict"} with respx.mock(base_url=DEFAULT_BASE) as r: route = r.post(path) route.side_effect = [httpx.Response(409, json=conflict), httpx.Response(200, json=body)] - with pytest.raises(LenzError): - call(client, idempotency_key="mine-1") call(client, idempotency_key="mine-1") assert [c.request.headers["Idempotency-Key"] for c in route.calls] == ["mine-1", "mine-1"] diff --git a/tests/test_idempotent_resend.py b/tests/test_idempotent_resend.py new file mode 100644 index 0000000..98da813 --- /dev/null +++ b/tests/test_idempotent_resend.py @@ -0,0 +1,254 @@ +"""A call that sends an ``Idempotency-Key``: + +* meets a 409 ``idempotency_conflict`` (the first request with that key is + still running) by sending the SAME key and body again, after the stated + wait or the usual backoff, within the call's retry budget; if it still + conflicts, the error is ``retryable``; +* puts the key on every ``LenzError`` it raises (``exc.idempotency_key``), so + a resend can reuse it and replay instead of running twice. +""" + +from __future__ import annotations + +import json + +import httpx +import pytest +import respx + +from lenz_io import ( + Lenz, + LenzConnectionError, + LenzError, + LenzPipelineError, + LenzRequestTimeoutError, + LenzTimeoutError, + LenzValidationError, + ReviewFailed, +) + +BASE = "https://lenz.io/api/v1" +_CONFLICT = {"detail": "A request with this Idempotency-Key is still being processed.", "code": "idempotency_conflict"} +_ACCEPTED = {"task_id": "t1", "claim_text": "A."} +_BATCH = {"batch_id": "b", "items": [{"task_id": "t1", "claim_text": "A."}]} + + +@pytest.fixture() +def slept(monkeypatch: pytest.MonkeyPatch) -> list[float]: + calls: list[float] = [] + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: calls.append(s)) + return calls + + +def _keys(route: respx.Route) -> list[str | None]: + return [c.request.headers.get("Idempotency-Key") for c in route.calls] + + +def _bodies(route: respx.Route) -> list[bytes]: + return [c.request.content for c in route.calls] + + +_CALLS = [ + ("/verify", _ACCEPTED, lambda c, **kw: c.verify("A.", **kw)), + ("/verify/batch", _BATCH, lambda c, **kw: c.verify_batch(claims=[{"claim": "A."}], **kw)), + ("/ask/v1", {"reply": "Yes."}, lambda c, **kw: c.ask.send("v1", message="Why?", **kw)), + ("/assess", {"claims": []}, lambda c, **kw: c.assess("A.", **kw)), + ("/extract", {"claims": []}, lambda c, **kw: c.extract(text="A.", **kw)), + ("/verify/t0/select", _BATCH, lambda c, **kw: c.select("t0", claims=["A."], **kw)), +] + + +@pytest.mark.parametrize(("path", "ok", "call"), _CALLS) +class TestAConflictIsSentAgainWithTheSameKey: + def test_then_succeeds(self, client: Lenz, slept: list[float], path, ok, call) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(path) + route.side_effect = [httpx.Response(409, json=_CONFLICT), httpx.Response(200, json=ok)] + call(client) + assert route.call_count == 2 + keys = _keys(route) + assert keys[0] and keys[0] == keys[1] + assert _bodies(route)[0] == _bodies(route)[1] + assert slept == [1.0], "the existing backoff" + + def test_a_stated_wait_is_honoured(self, client: Lenz, slept: list[float], path, ok, call) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(path) + route.side_effect = [ + httpx.Response(409, json=_CONFLICT, headers={"Retry-After": "3"}), + httpx.Response(200, json=ok), + ] + call(client) + assert slept == [3.0] + + def test_a_caller_key_is_kept(self, client: Lenz, slept: list[float], path, ok, call) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(path) + route.side_effect = [httpx.Response(409, json=_CONFLICT), httpx.Response(200, json=ok)] + call(client, idempotency_key="mine") + assert _keys(route) == ["mine", "mine"] + + def test_still_conflicting_raises_retryable_with_the_key( + self, client: Lenz, slept: list[float], path, ok, call + ) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(path).respond(409, json=_CONFLICT) + with pytest.raises(LenzError) as ei: + call(client) + assert route.call_count == 4, "the call's retry budget: 1 + max_retries" + assert len(set(_keys(route))) == 1 + err = ei.value + assert type(err) is LenzError + assert err.status_code == 409 + assert err.retryable is True + assert err.idempotency_key == _keys(route)[0] + # The 2.x attributes of that error are unchanged. + assert err.code == "" + assert err.message == _CONFLICT["detail"] + + def test_without_a_key_a_conflict_is_not_resent(self, client: Lenz, slept: list[float], path, ok, call) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(path).respond(409, json=_CONFLICT) + with pytest.raises(LenzError) as ei: + call(client, idempotency=False) + assert route.call_count == 1 + assert ei.value.idempotency_key is None + + +def test_the_retry_budget_bounds_the_resends(slept: list[float]) -> None: + with Lenz(api_key="lenz_test", max_retries=0) as client, respx.mock(base_url=BASE) as r: + route = r.post("/verify").respond(409, json=_CONFLICT) + with pytest.raises(LenzError): + client.verify("A.") + assert route.call_count == 1 + + +def test_a_long_stated_wait_is_capped(client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify") + route.side_effect = [ + httpx.Response(409, json=_CONFLICT, headers={"Retry-After": "3600"}), + httpx.Response(200, json=_ACCEPTED), + ] + client.verify("A.") + assert slept == [1.0], "a wait past the cap falls back to the backoff" + + +def test_other_409s_are_not_resent(client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify/t0/select").respond(409, json={"detail": "Nothing to select.", "code": "x"}) + with pytest.raises(LenzError): + client.select("t0", claims=["A."]) + assert route.call_count == 1 + + +class TestReviewAndCitecheck: + def test_a_conflict_naming_the_review_returns_it_at_once(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/review").respond(409, json={**_CONFLICT, "review_id": "r1"}) + started = client.review("Draft.") + assert started.review_id == "r1" + assert route.call_count == 1 + + def test_a_conflict_naming_the_check_returns_it_at_once(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/citecheck").respond(409, json={**_CONFLICT, "citecheck_id": "c1"}) + started = client.citecheck("Draft.") + assert started.citecheck_id == "c1" + assert route.call_count == 1 + + def test_a_conflict_without_the_id_is_sent_again(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/review") + route.side_effect = [ + httpx.Response(409, json=_CONFLICT), + httpx.Response(202, json={"review_id": "r1", "status": "queued"}), + ] + started = client.review("Draft.") + assert started.review_id == "r1" + keys = _keys(route) + assert keys[0] == keys[1] + + +class TestTheErrorCarriesTheKey: + def test_a_refused_call(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify").respond(422, json={"detail": "Bad.", "code": "validation_error"}) + with pytest.raises(LenzValidationError) as ei: + client.verify("A.") + assert ei.value.idempotency_key == route.calls.last.request.headers["Idempotency-Key"] + + @pytest.mark.parametrize( + ("failure", "cls"), + [(httpx.ConnectError("refused"), LenzConnectionError), (httpx.ReadTimeout("slow"), LenzRequestTimeoutError)], + ) + def test_a_connection_failure(self, client: Lenz, slept: list[float], failure, cls) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/assess").mock(side_effect=failure) + with pytest.raises(cls) as ei: + client.assess("A.", idempotency_key="k-1") + assert ei.value.idempotency_key == "k-1" + + def test_the_wait_of_a_submit_and_wait_call(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + submit = r.post("/verify").respond(200, json=_ACCEPTED) + r.get("/verify/status/t1").respond(200, json={"status": "failed", "failure_reason": "x"}) + with pytest.raises(LenzPipelineError) as ei: + client.verify_and_wait("A.") + assert ei.value.idempotency_key == submit.calls.last.request.headers["Idempotency-Key"] + + def test_a_wait_timeout_of_a_submit_and_wait_call(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/verify").respond(200, json=_ACCEPTED) + r.get("/verify/status/t1").respond(200, json={"status": "processing", "progress": {}}) + with pytest.raises(LenzTimeoutError) as ei: + client.verify_and_wait("A.", timeout=0, idempotency_key="k-2") + assert ei.value.idempotency_key == "k-2" + + def test_a_failed_review(self, client: Lenz, slept: list[float]) -> None: + failed = { + "review_id": "r1", + "status": "failed", + "issues": [], + "failures": [], + "claims": [], + "failure": {"failure_reason": "internal"}, + } + with respx.mock(base_url=BASE) as r: + submit = r.post("/review").respond(202, json={"review_id": "r1", "status": "queued"}) + r.get("/reviews/r1").respond(200, json=failed) + with pytest.raises(ReviewFailed) as ei: + client.review_and_wait("Draft.") + assert ei.value.idempotency_key == submit.calls.last.request.headers["Idempotency-Key"] + + def test_a_batch_submit_and_wait(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/verify/batch").respond(402, json={"detail": "No credits.", "code": "no_credits"}) + with pytest.raises(LenzError) as ei: + client.verify_batch_and_wait(claims=[{"claim": "A."}], idempotency_key="k-3") + assert ei.value.idempotency_key == "k-3" + + def test_a_call_that_sent_no_key(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + r.get("/me/usage").respond(404, json={"detail": "x"}) + r.get("/verify/status/t1").respond(404, json={"detail": "x"}) + r.post("/verify").respond(422, json={"detail": "Bad."}) + with pytest.raises(LenzError) as usage: + client.usage() + with pytest.raises(LenzError) as wait: + client.wait("t1") + with pytest.raises(LenzError) as opted_out: + client.verify("A.", idempotency=False) + assert usage.value.idempotency_key is None + assert wait.value.idempotency_key is None + assert opted_out.value.idempotency_key is None + + +def test_the_body_sent_again_is_byte_identical(client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post("/verify/batch") + route.side_effect = [httpx.Response(409, json=_CONFLICT), httpx.Response(200, json=_BATCH)] + client.verify_batch(claims=[{"claim": "A.", "depth": "low"}], language="de") + first, second = _bodies(route) + assert first == second + assert json.loads(first)["language"] == "de" From 1e7fba048c308737bc8ba4dd656cab8ca26b55c4 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:33:44 +0300 Subject: [PATCH 33/67] fix: waits work with timeout=None or an httpx.Timeout, and no poll runs past the deadline One helper derives each poll's timeout from the client in use (its read timeout; None means no client cap; an injected http_client keeps its own), fixing the TypeError a non-float Lenz(timeout=) raised in every wait. A poll is bounded by what is left of the deadline and none starts once it is spent (the first poll of a wait still always runs, so timeout=0 reads once as in 2.x); the ids left are timed out. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 47 ++++++++++--- tests/test_review.py | 5 +- tests/test_wait_stops.py | 141 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 176 insertions(+), 17 deletions(-) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 7cc27a7..6af50ae 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -643,7 +643,7 @@ def __init__( *, api_key: str | None = None, base_url: str | None = None, - timeout: float = DEFAULT_TIMEOUT, + timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, max_retries: int = DEFAULT_MAX_RETRIES, http_client: httpx.Client | None = None, user_agent: str | None = None, @@ -1575,15 +1575,21 @@ def _wait_job( deadline = time.monotonic() + timeout last: _Job | None = None last_dump: dict[str, Any] | None = None + first = True while True: # One request per poll, bounded by what is left of the deadline: # the client's own retry ladder inside a poll could run minutes # past it. A failed poll is retried on the next round instead. + # Once the deadline is spent no poll starts (the first one always + # runs, as in 2.x). stated_wait: float | None = None job: _Job | None = None remaining = deadline - time.monotonic() + if remaining <= 0 and not first: + raise timed_out(last) + first = False try: - body = self._request("GET", path, max_retries=0, timeout=max(1.0, min(remaining, self._timeout))) + body = self._request("GET", path, max_retries=0, timeout=self._poll_timeout(remaining)) job = parse(body) if job is None: raise ValueError("not this job's body") @@ -1636,14 +1642,13 @@ def _poll_to_terminal( task has no ``TaskStatus`` — ``"timeout"`` is a client-side concept, never a wire status — so it lands in the second set, not the dict. - Ordering preserves the legacy ``verify_and_wait`` behavior: each round - polls every still-pending id once *before* the deadline check, so after - sleeping the remaining time we always poll once more and can succeed - just past the nominal deadline. Each poll is ONE request whose timeout - is what is left of the deadline (at least 1s, at most the client's - timeout): the client's own retry ladder inside a poll could run - minutes past it, so a failed poll is retried on the next round - instead. Backoff reuses the existing 2/4/8/8…s sequence; the 10s cap + The first round polls every id once whatever the deadline (as in + 2.x, ``timeout=0`` reads each status once). After it no poll starts + once the deadline is spent: the ids left are timed out. Each poll is + ONE request whose timeout is what is left of the deadline, at most the + client's own timeout (``_poll_timeout``): the client's own retry + ladder inside a poll could run minutes past it, so a failed poll is + retried on the next round instead. Backoff reuses the existing 2/4/8/8…s sequence; the 10s cap is currently unreachable and kept only to preserve identical timing. A per-id poll that fails with a 5xx, a 429, a network failure or any @@ -1672,18 +1677,23 @@ def _poll_to_terminal( timed_out: set[str] = set() deadline = time.monotonic() + timeout backoff_idx = 0 + first_round = True while pending: still_pending: list[str] = [] server_hint: float | None = None stated_wait: float | None = None for task_id in pending: remaining = deadline - time.monotonic() + if remaining <= 0 and not first_round: + # The deadline is spent: no poll starts past it. + still_pending.append(task_id) + continue try: body = self._request( "GET", f"/verify/status/{task_id}", max_retries=0, - timeout=max(1.0, min(remaining, self._timeout)), + timeout=self._poll_timeout(remaining), ) status = TaskStatus.model_validate(body) except (LenzGoneError, LenzAuthError, LenzNotFoundError) as exc: @@ -1720,6 +1730,7 @@ def _poll_to_terminal( except Exception: logger.debug("on_progress callback raised for task %s", task_id, exc_info=True) pending = still_pending + first_round = False if not pending: break remaining = deadline - time.monotonic() @@ -1873,6 +1884,20 @@ def _timeout_at_least(self, floor: float) -> float | None: return None return floor + def _poll_timeout(self, remaining: float) -> float | None: + """The timeout of one poll request: what is left of the wait's + deadline, at most the client's own read timeout (``None`` there means + no client cap). Past the deadline (only the first poll of a wait runs + then) the client's own timeout, ``None`` meaning the client default. + + Reads the client actually in use, so ``Lenz(timeout=None)``, an + ``httpx.Timeout`` and an ``httpx.Client`` passed as ``http_client=`` + all work.""" + cap = self._client.timeout.read + if remaining <= 0: + return cap + return remaining if cap is None else min(remaining, cap) + def _extract( self, *, diff --git a/tests/test_review.py b/tests/test_review.py index aabf0d7..aaf8f47 100644 --- a/tests/test_review.py +++ b/tests/test_review.py @@ -527,9 +527,10 @@ def test_timeout_raises_with_the_last_body(self, client, monkeypatch): assert isinstance(err, LenzTimeoutError) assert err.review_id == "442b6aa9" assert isinstance(err.partial, ReviewFull) and err.partial.status == "verifying" - # never sleeps past the deadline, and polls once more at it + # never sleeps past the deadline, and starts no poll once it is spent + # (3.0; 2.x polled once more at the deadline, past it) assert clock[0] == 40 - assert get.call_count == 4 + assert get.call_count == 3 def test_timeout_before_any_body_has_no_partial(self, client, monkeypatch): clock = [0.0] diff --git a/tests/test_wait_stops.py b/tests/test_wait_stops.py index ba9d896..a44299c 100644 --- a/tests/test_wait_stops.py +++ b/tests/test_wait_stops.py @@ -145,13 +145,16 @@ def test_the_request_timeout_never_runs_past_the_deadline(self, slept: list[floa timeouts = [c.request.extensions["timeout"]["read"] for c in poll.calls] assert all(t <= 5 for t in timeouts), timeouts - def test_the_final_poll_still_runs_with_a_floor(self, slept: list[float]) -> None: - # Past the deadline the loop still polls once more, with at least 1s. - with Lenz(api_key="lenz_test") as client, respx.mock(base_url=BASE) as r: + def test_a_zero_timeout_still_reads_once(self, slept: list[float]) -> None: + # As in 2.x, ``timeout=0`` reads the status once (bounded by the + # client timeout, there being no deadline left to bound it), then stops. + with Lenz(api_key="lenz_test", timeout=20.0) as client, respx.mock(base_url=BASE) as r: poll = r.get("/verify/status/t").respond(200, json=_RUNNING) with pytest.raises(LenzTimeoutError): client.wait("t", timeout=0) - assert poll.calls.last.request.extensions["timeout"]["read"] == 1.0 + assert poll.call_count == 1 + assert poll.calls.last.request.extensions["timeout"]["read"] == 20.0 + assert slept == [] def test_the_client_timeout_bounds_a_long_wait(self, slept: list[float]) -> None: with Lenz(api_key="lenz_test", timeout=20.0) as client, respx.mock(base_url=BASE) as r: @@ -190,3 +193,133 @@ def test_a_connection_error_mid_wait_is_retried(self, client: Lenz, slept: list[ with pytest.raises(LenzError): client.citecheck_and_wait("Draft.", timeout=300) assert poll.call_count == 2 + + +@pytest.fixture() +def clock(monkeypatch: pytest.MonkeyPatch) -> list[float]: + """A fake clock: ``time.sleep`` moves it, requests take no time unless a + test moves it.""" + now = [0.0] + monkeypatch.setattr("lenz_io.client.time.monotonic", lambda: now[0]) + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: now.__setitem__(0, now[0] + s)) + return now + + +def _read_timeouts(route: respx.Route) -> list[float | None]: + return [c.request.extensions["timeout"]["read"] for c in route.calls] + + +class TestNoPollPastTheDeadline: + def test_no_poll_starts_once_the_deadline_is_spent(self, clock: list[float]) -> None: + with Lenz(api_key="lenz_test", timeout=30.0) as client, respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_RUNNING) + with pytest.raises(LenzTimeoutError): + client.wait("t", timeout=5) + # t=0 (5s left), sleep 2, t=2 (3s left), sleep 3, t=5: nothing left. + assert _read_timeouts(poll) == [5.0, 3.0] + assert clock[0] == 5.0 + + def test_a_smaller_client_timeout_bounds_every_poll(self, clock: list[float]) -> None: + with Lenz(api_key="lenz_test", timeout=2.5) as client, respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_RUNNING) + with pytest.raises(LenzTimeoutError): + client.wait("t", timeout=5) + assert _read_timeouts(poll) == [2.5, 2.5] + + def test_a_batch_marks_the_items_it_had_no_time_for_as_timed_out(self, client: Lenz, clock: list[float]) -> None: + polls: list[str] = [] + + def answer(request: httpx.Request) -> httpx.Response: + polls.append(request.url.path.rsplit("/", 1)[-1]) + if len(polls) == 3: + clock[0] += 100.0 # the second round's first poll is slow + return httpx.Response(200, json=_RUNNING) + + with respx.mock(base_url=BASE) as r: + r.post("/verify/batch").respond( + 200, json={"batch_id": "b", "items": [{"task_id": "a", "claim": "A."}, {"task_id": "b", "claim": "B."}]} + ) + r.get(url__regex=r"/verify/status/.*").mock(side_effect=answer) + results = client.verify_batch_and_wait(claims=[{"claim": "A."}, {"claim": "B."}], timeout=10) + assert polls == ["a", "b", "a"], "b is not polled after the deadline" + assert [x.status for x in results] == ["timeout", "timeout"] + + @pytest.mark.parametrize( + ("submit", "path", "body", "call"), + [ + ( + "/review", + "/reviews/r1", + {"review_id": "r1", "status": "verifying", "issues": [], "failures": [], "claims": []}, + lambda c: c.review_and_wait("Draft.", timeout=12), + ), + ( + "/citecheck", + "/citechecks/c1", + { + "citecheck_id": "c1", + "status": "checking", + "citations": [], + "citation_issues": [], + "citation_failures": [], + }, + lambda c: c.citecheck_and_wait("Draft.", timeout=12), + ), + ], + ) + def test_job_waits_stop_at_the_deadline(self, clock: list[float], submit, path, body, call) -> None: + accepted = {"review_id": "r1", "citecheck_id": "c1", "status": "queued"} + with Lenz(api_key="lenz_test", timeout=30.0) as client, respx.mock(base_url=BASE) as r: + r.post(submit).respond(202, json=accepted) + poll = r.get(path).respond(200, json=body) + with pytest.raises(LenzTimeoutError): + call(client) + # t=0 (12 left), sleep 10, t=10 (2 left), sleep 2, t=12: nothing left. + assert _read_timeouts(poll) == [12.0, 2.0] + + +class TestTheClientTimeoutSetting: + """The per-poll bound reads the client's own timeout, whatever form it + took (2.x accepted ``None`` and an ``httpx.Timeout`` there).""" + + @pytest.mark.parametrize( + ("timeout", "expected"), + [(None, 300.0), (4.0, 4.0), (httpx.Timeout(7.0), 7.0), (httpx.Timeout(10.0, read=6.0), 6.0)], + ) + def test_wait(self, slept: list[float], timeout, expected) -> None: + with Lenz(api_key="lenz_test", timeout=timeout) as client, respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_DONE) + client.wait("t", timeout=300) + assert _read_timeouts(poll) == [pytest.approx(expected, abs=1.0)] + + @pytest.mark.parametrize("timeout", [None, httpx.Timeout(7.0)]) + def test_review_and_citecheck_waits(self, slept: list[float], timeout) -> None: + done_review = {"review_id": "r1", "status": "completed", "issues": [], "failures": [], "claims": []} + done_check = { + "citecheck_id": "c1", + "status": "completed", + "citations": [], + "citation_issues": [], + "citation_failures": [], + } + with Lenz(api_key="lenz_test", timeout=timeout) as client, respx.mock(base_url=BASE) as r: + r.post("/review").respond(202, json={"review_id": "r1", "status": "queued"}) + r.get("/reviews/r1").respond(200, json=done_review) + r.post("/citecheck").respond(202, json={"citecheck_id": "c1", "status": "queued"}) + r.get("/citechecks/c1").respond(200, json=done_check) + assert client.review_and_wait("Draft.").status == "completed" + assert client.citecheck_and_wait("Draft.").status == "completed" + + def test_an_injected_http_client_keeps_its_own_timeout(self, slept: list[float]) -> None: + with httpx.Client(timeout=9.0) as http, Lenz(api_key="lenz_test", http_client=http) as client: + with respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_DONE) + client.wait("t", timeout=300) + assert _read_timeouts(poll) == [9.0] + + def test_an_injected_unbounded_http_client(self, slept: list[float]) -> None: + with httpx.Client(timeout=None) as http, Lenz(api_key="lenz_test", http_client=http) as client: + with respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_DONE) + client.wait("t", timeout=300) + assert _read_timeouts(poll) == [pytest.approx(300.0, abs=1.0)] From 94dc2ccb47aa20710d03fea89f2c9c074dbd074c Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:36:16 +0300 Subject: [PATCH 34/67] fix: a batch wait fails the one item a version error or 404 answers, and raises a 401 or 403 A LenzApiVersionError, 404 or 410 on one item's poll is that item's outcome (failed, no status_detail); the others keep being polled. A 401 / 403 refuses the key itself, so the batch wait raises it. A single wait still raises every one of them. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 36 +++++++++++++++++------------- tests/test_api_version_guard.py | 12 +++++++--- tests/test_wait_stops.py | 39 +++++++++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 6af50ae..3e1f7f7 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -1126,9 +1126,11 @@ def verify_batch_and_wait( order**. Never raises on a per-item outcome — a claim that fails, pauses for input, or times out becomes a ``BatchItemResult`` with the matching ``status`` rather than an exception. (Transport/auth errors on the - initial submit still raise.) An item whose poll is answered 401 / 403 / - 404 or 410 is ``failed`` at once, with no ``status_detail``; the other - items keep being polled. + initial submit still raise.) An item whose poll is answered 404 or 410, + or in another API version (``LenzApiVersionError``), is ``failed`` at + once, with no ``status_detail``; the other items keep being polled. A + poll answered 401 / 403 refuses the key itself, so the whole call + raises ``LenzAuthError``. ``on_progress(task_id, progress)`` fires per still-running item per round; the ``task_id`` is what tells you which claim moved. @@ -1157,8 +1159,8 @@ def verify_batch_and_wait( status = terminal.get(it.task_id) if it.task_id in stopped: # An error no later poll could change: removed by the - # account's retention period (410), a refused key (401 / 403) - # or an unknown task (404). Terminal, with no status to carry. + # account's retention period (410), an unknown task (404) or + # an answer in another API version. Terminal, with no status. results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="failed")) elif not it.task_id or it.task_id in timed_out or status is None: results.append(BatchItemResult(task_id=it.task_id, claim_text=it.claim_text, status="timeout")) @@ -1655,10 +1657,12 @@ def _poll_to_terminal( other error a later poll can change does not abort the other ids: that id stays pending and is polled next round (after the wait the server stated, if it stated one), so a persistent one surfaces as a timeout - once the deadline passes. An error no later poll can change stops that - id at once and lands in ``stopped_by_id``: a 410 (``LenzGoneError``, - retention removed it), a 401 / 403 (``LenzAuthError``) and a 404 - (``LenzNotFoundError``). A version error is raised for the whole wait. + once the deadline passes. An error no later poll can change for that + id stops it at once and lands in ``stopped_by_id``: a 410 + (``LenzGoneError``, retention removed it), a 404 + (``LenzNotFoundError``) and a version error (``LenzApiVersionError``). + A 401 / 403 (``LenzAuthError``) refuses the key itself, so it is raised + for the whole wait. ``wait`` raises a stopped id's error. ``on_progress(task_id, progress)`` fires once per still-running poll. It takes the id as well as the object because this loop round-robins a @@ -1696,14 +1700,16 @@ def _poll_to_terminal( timeout=self._poll_timeout(remaining), ) status = TaskStatus.model_validate(body) - except (LenzGoneError, LenzAuthError, LenzNotFoundError) as exc: - # Terminal: no later poll will say otherwise. (The API - # never answers 410 for a task that is still running.) + except LenzAuthError: + # The key itself is refused: every other id would answer + # the same, so the whole wait ends with it. + raise + except (LenzGoneError, LenzNotFoundError, LenzApiVersionError) as exc: + # Terminal for this id: no later poll will say otherwise. + # (The API never answers 410 for a task that is still + # running; a version error answers the same again.) stopped[task_id] = exc continue - except LenzApiVersionError: - # Not a failed poll: every later poll would answer the same. - raise except LenzError as exc: # Don't let one id's poll failure abort the rest — retry it # next round (bounded by the deadline below), after the diff --git a/tests/test_api_version_guard.py b/tests/test_api_version_guard.py index 4dfb481..faaed4f 100644 --- a/tests/test_api_version_guard.py +++ b/tests/test_api_version_guard.py @@ -144,10 +144,16 @@ def test_the_error_is_exported_and_pickles_its_fields() -> None: assert json.dumps(exc.body) -def test_waiting_on_a_batch_does_not_swallow_the_version_error(client: Lenz) -> None: +def test_waiting_never_swallows_the_version_error(client: Lenz) -> None: + # A version error is never read as a slow poll: it stops that id at once + # (``wait`` raises it; a batch marks that item failed). with respx.mock(base_url=DEFAULT_BASE_URL) as mock: - mock.get(url__regex=r".*/verify/.*").respond( + route = mock.get(url__regex=r".*/verify/.*").respond( 200, json={"status": "processing"}, headers={"X-Lenz-API-Version": OLD} ) + terminal, timed_out, stopped = client._poll_to_terminal(["a" * 32], 5) with pytest.raises(LenzApiVersionError): - client._poll_to_terminal(["a" * 32], 5) + client.wait("a" * 32, timeout=5) + assert isinstance(stopped["a" * 32], LenzApiVersionError) + assert terminal == {} and timed_out == set() + assert route.call_count == 2 diff --git a/tests/test_wait_stops.py b/tests/test_wait_stops.py index a44299c..d21b554 100644 --- a/tests/test_wait_stops.py +++ b/tests/test_wait_stops.py @@ -16,6 +16,7 @@ from lenz_io import ( Lenz, + LenzApiVersionError, LenzAuthError, LenzError, LenzNotFoundError, @@ -63,16 +64,21 @@ def test_verify_and_wait_raises_it(self, client: Lenz, slept: list[float], statu assert poll.call_count == 1 assert slept == [] - def test_a_batch_item_fails_and_the_others_continue( - self, client: Lenz, slept: list[float], status, body, cls - ) -> None: - with respx.mock(base_url=BASE) as r: + def test_a_batch(self, client: Lenz, slept: list[float], status, body, cls) -> None: + # A 404 is that item's outcome; a 401 / 403 is the whole account's, so + # the batch wait raises it (before the other item is polled). + with respx.mock(base_url=BASE, assert_all_called=False) as r: r.post("/verify/batch").respond( 200, json={"batch_id": "b", "items": [{"task_id": "a", "claim": "A."}, {"task_id": "t", "claim": "B."}]} ) bad = r.get("/verify/status/a").respond(status, json=body) good = r.get("/verify/status/t") good.side_effect = [httpx.Response(200, json=_RUNNING), httpx.Response(200, json=_DONE)] + if status in (401, 403): + with pytest.raises(cls): + client.verify_batch_and_wait(claims=[{"claim": "A."}, {"claim": "B."}], timeout=300) + assert bad.call_count == 1 + return results = client.verify_batch_and_wait(claims=[{"claim": "A."}, {"claim": "B."}], timeout=300) assert [x.status for x in results] == ["failed", "completed"] assert results[0].status_detail is None @@ -81,6 +87,31 @@ def test_a_batch_item_fails_and_the_others_continue( assert good.call_count == 2 +_OLD = {"X-Lenz-API-Version": "2026-05-13"} + + +class TestVersionErrorsInAWait: + def test_a_batch_item_fails_and_the_others_continue(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/verify/batch").respond( + 200, json={"batch_id": "b", "items": [{"task_id": "a", "claim": "A."}, {"task_id": "t", "claim": "B."}]} + ) + bad = r.get("/verify/status/a").respond(200, json=_DONE, headers=_OLD) + good = r.get("/verify/status/t") + good.side_effect = [httpx.Response(200, json=_RUNNING), httpx.Response(200, json=_DONE)] + results = client.verify_batch_and_wait(claims=[{"claim": "A."}, {"claim": "B."}], timeout=300) + assert [x.status for x in results] == ["failed", "completed"] + assert results[0].status_detail is None + assert bad.call_count == 1 + + def test_a_single_wait_raises_it(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_DONE, headers=_OLD) + with pytest.raises(LenzApiVersionError): + client.wait("t", timeout=300) + assert poll.call_count == 1 + + class TestTransientErrorsAreRetried: @pytest.mark.parametrize( "failure", From 3d0ca6ed58e9c7a9efbd4707c5c3f62f17ac8d95 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:37:44 +0300 Subject: [PATCH 35/67] fix: iter() stops at the total, on a page it did not ask for, or without a page size, and refuses a start page below 1 verifications.iter() and library.iter() also stop once the pages read reach the response's total, when the response states no usable page_size, and (without yielding it) when the server answers another page than the one asked for. A start page below 1 raises ValueError at call time, as sort="random" does. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 36 +++++++++++++++++---- tests/test_pagination_iterators.py | 51 ++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 3e1f7f7..326fd7e 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -329,14 +329,16 @@ def iter(self, *, page: int = 1) -> Iterator[VerificationListItem]: Fetches a page only when the items before it have been consumed, reads the page size from each response, and stops after a short or empty - page:: + page, once the pages read reach the response's ``total``, or when the + server answers another page than the one asked for. ``page`` must be + 1 or more (``ValueError``):: for item in client.verifications.iter(): print(item.verification_id, item.verdict) Since 3.0. ``list(page=...)`` reads one page. """ - return _walk(lambda n: self.list(page=n), page) + return _walk(lambda n: self.list(page=n), _first_page(page)) def get(self, verification_id: str) -> Verification: """Fetch a single verification by id. @@ -563,12 +565,15 @@ def iter( Fetches a page only when the items before it have been consumed, reads the page size from each response, and stops after a short or empty - page. ``sort="random"`` raises ``ValueError``: a random order is drawn + page, once the pages read reach the response's ``total``, or when the + server answers another page than the one asked for. ``page`` must be + 1 or more, and ``sort="random"`` raises ``ValueError``: a random order is drawn anew for every page, so walking it neither reaches every item nor avoids repeats (use ``list(sort="random")`` for a sample). Since 3.0. """ if sort == "random": raise ValueError('iter cannot walk sort="random" (each page is a fresh sample); call library.list instead.') + page = _first_page(page) return _walk( lambda n: self.list( page=n, sort=sort, search=search, domain=domain, entity=entity, curated=curated, verdict=verdict @@ -578,17 +583,34 @@ def iter( def _walk(fetch: Callable[[int], VerificationList | LibraryList], page: int) -> Iterator[Any]: - """The items of ``fetch(page)``, ``fetch(page + 1)``, ... until a page - comes back shorter than its own ``page_size`` (or empty). A generator: no - page is fetched before its first item is asked for.""" + """The items of ``fetch(page)``, ``fetch(page + 1)``, ... A generator: no + page is fetched before its first item is asked for. + + Stops after a page that is short or empty, once the pages read reach the + response's ``total``, when a response states no usable ``page_size``, and + (without yielding it) when the server answers another page than the one + asked for, which a server clamping a page past the end would repeat.""" while True: current = fetch(page) + sent = current.model_fields_set + if "page" in sent and current.page != page: + return yield from current.items - if not current.items or len(current.items) < current.page_size: + size = current.page_size if "page_size" in sent else 0 + if not current.items or size <= 0 or len(current.items) < size: + return + if "total" in sent and page * size >= current.total: return page += 1 +def _first_page(page: int) -> int: + """``page`` for an iterator, refused unless it is 1 or more.""" + if isinstance(page, bool) or not isinstance(page, int) or page < 1: + raise ValueError(f"iter starts at page 1 or later (got page={page!r}).") + return page + + def _call_key(idempotency_key: str | None, idempotency: bool) -> str | None: """The ``Idempotency-Key`` for one call: the caller's own, else a random one generated once per call (so every retry of that call carries it), else diff --git a/tests/test_pagination_iterators.py b/tests/test_pagination_iterators.py index 6941056..95b7c2c 100644 --- a/tests/test_pagination_iterators.py +++ b/tests/test_pagination_iterators.py @@ -88,6 +88,57 @@ def test_no_prefetch(self, client: Lenz, path, walk, item_type) -> None: next(it) assert route.call_count == 2 + def test_it_stops_when_the_pages_read_reach_the_total(self, client: Lenz, path, walk, item_type) -> None: + # A full page that ends the list: no request for the empty one after it. + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {1: _page(0, 2, 1, 2, total=4), 2: _page(2, 2, 2, 2, total=4)}) + assert len(list(walk(client))) == 4 + assert route.call_count == 2 + + def test_it_stops_when_the_server_answers_another_page(self, client: Lenz, path, walk, item_type) -> None: + # A server that clamps a page past the end to the last one would + # otherwise repeat that page for ever. Its items are not yielded again. + first, clamped = _page(0, 2, 1, 2), _page(0, 2, 1, 2) + del first["total"], clamped["total"] + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {1: first, 2: clamped}) + items = list(walk(client)) + assert [i.verification_id for i in items] == ["v0", "v1"] + assert route.call_count == 2 + + @pytest.mark.parametrize("page_size", [0, -1, None]) + def test_it_stops_without_a_usable_page_size(self, client: Lenz, path, walk, item_type, page_size) -> None: + page = _page(0, 2, 1, 2) + del page["total"] + if page_size is None: + del page["page_size"] + else: + page["page_size"] = page_size + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {1: page}) + assert len(list(walk(client))) == 2 + assert route.call_count == 1 + + def test_a_missing_total_is_not_read_as_zero(self, client: Lenz, path, walk, item_type) -> None: + first, second = _page(0, 2, 1, 2), _page(2, 1, 2, 2) + del first["total"], second["total"] + with respx.mock(base_url=BASE) as r: + route = r.get(path) + _pager(route, {1: first, 2: second}) + assert len(list(walk(client))) == 3 + assert route.call_count == 2 + + @pytest.mark.parametrize("page", [0, -3]) + def test_the_start_page_must_be_one_or_more(self, client: Lenz, path, walk, item_type, page) -> None: + with respx.mock(base_url=BASE, assert_all_called=False) as r: + route = r.get(path) + with pytest.raises(ValueError, match="page"): + walk(client, page=page) + assert route.call_count == 0 + class TestFilters: def test_library_filters_reach_every_page(self, unauth_client: Lenz) -> None: From 4104b7d45bf9c2302d7b6665e3283ebb81f29078 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:39:01 +0300 Subject: [PATCH 36/67] fix: every transport failure worth resending is a LenzConnectionError, retried and polled through httpx.TransportError (a server that hung up, a proxy failure, a read or write error) is retried like a network error and raised as LenzConnectionError; a wait polls again after one instead of aborting. UnsupportedProtocol and LocalProtocolError (a request that could never be sent) stay the httpx exception. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 9 +++- src/lenz_io/errors.py | 3 +- tests/test_transport_errors.py | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/test_transport_errors.py diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 326fd7e..e0f849a 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -2085,7 +2085,14 @@ def _send( response = self._client.request( method, url, json=json, params=params, headers=req_headers, timeout=req_timeout ) - except (httpx.TimeoutException, httpx.NetworkError) as exc: + except (httpx.UnsupportedProtocol, httpx.LocalProtocolError): + # The request could never be sent (a bad URL scheme, a request + # httpx refuses to write): a programming error, not a network one. + raise + except httpx.TransportError as exc: + # Connect / read / write failures, timeouts, a server that + # hung up (RemoteProtocolError), a proxy failure: worth sending + # again. last_exc = exc if attempt >= retries: # Subclasses of LenzAPIError, which is what 2.x raised. diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index d592754..97bc638 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -290,7 +290,8 @@ def _derived_retryable(self) -> bool | None: class LenzConnectionError(LenzAPIError): """The request never got an answer: the connection failed or broke - (DNS, refused, reset, TLS), after the SDK's own retries. + (DNS, refused, reset, TLS, a server or proxy that hung up), after the + SDK's own retries. A subclass of :class:`LenzAPIError`, which is what 2.x raised, so an existing ``except LenzAPIError`` keeps catching it. ``__cause__`` is the diff --git a/tests/test_transport_errors.py b/tests/test_transport_errors.py new file mode 100644 index 0000000..a8dafe0 --- /dev/null +++ b/tests/test_transport_errors.py @@ -0,0 +1,80 @@ +"""Every transport failure of a request that may succeed when sent again (a +dropped connection, a server that hung up, a proxy failure) is a +``LenzConnectionError``, retried like a network error, and a wait polls again +after one. A request that could never be sent (an unsupported URL scheme, a +request httpx refuses to write) is a programming error and stays the httpx +exception.""" + +from __future__ import annotations + +import httpx +import pytest +import respx + +from lenz_io import Lenz, LenzAPIError, LenzConnectionError, LenzRequestTimeoutError + +BASE = "https://lenz.io/api/v1" +_USAGE = {"credits": {"remaining": 1}} +_DONE = {"status": "completed", "task_id": "t", "result": {"verification_id": "v1", "claim": "A."}} + + +@pytest.fixture(autouse=True) +def _no_sleep(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: None) + + +_TRANSIENT = [ + httpx.RemoteProtocolError("Server disconnected without sending a response."), + httpx.ProxyError("proxy refused"), + httpx.ReadError("reset"), +] + + +@pytest.mark.parametrize("failure", _TRANSIENT) +def test_raised_as_a_connection_error_after_the_retries(client: Lenz, failure: Exception) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get("/me/usage").mock(side_effect=failure) + with pytest.raises(LenzConnectionError) as ei: + client.usage() + assert route.call_count == 4 + err = ei.value + assert type(err) is LenzConnectionError + assert isinstance(err, LenzAPIError) and not isinstance(err, LenzRequestTimeoutError) + assert err.__cause__ is failure + assert err.retryable is True + + +@pytest.mark.parametrize("failure", _TRANSIENT) +def test_retried_like_a_network_error(client: Lenz, failure: Exception) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get("/me/usage") + route.side_effect = [failure, httpx.Response(200, json=_USAGE)] + client.usage() + assert route.call_count == 2 + + +@pytest.mark.parametrize("failure", [httpx.UnsupportedProtocol("ftp"), httpx.LocalProtocolError("bad header")]) +def test_a_request_that_cannot_be_sent_is_not_wrapped(client: Lenz, failure: Exception) -> None: + with respx.mock(base_url=BASE) as r: + route = r.get("/me/usage").mock(side_effect=failure) + with pytest.raises(type(failure)): + client.usage() + assert route.call_count == 1 + + +def test_a_server_that_hangs_up_mid_wait_is_polled_again(client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t") + poll.side_effect = [httpx.RemoteProtocolError("Server disconnected"), httpx.Response(200, json=_DONE)] + assert client.wait("t", timeout=300).verification_id == "v1" + assert poll.call_count == 2 + + +def test_a_server_that_hangs_up_mid_review_wait_is_polled_again(client: Lenz) -> None: + done = {"review_id": "r1", "status": "completed", "issues": [], "failures": [], "claims": []} + with respx.mock(base_url=BASE) as r: + r.post("/review").respond(202, json={"review_id": "r1", "status": "queued"}) + poll = r.get("/reviews/r1") + poll.side_effect = [httpx.RemoteProtocolError("Server disconnected"), httpx.Response(200, json=done)] + assert client.review_and_wait("Draft.").status == "completed" + assert poll.call_count == 2 From 3e1bb57d85d91f96207c5578fb8876151e36d91f Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:40:18 +0300 Subject: [PATCH 37/67] fix: VerificationCompleted.verification.result reads the webhook defaults on a sparse payload A key the payload left out reads as event.result (and the Node SDK) read it: visibility "private", depth "standard", created_at "" and the rest, in both payload shapes, never the Verification model's own defaults. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/webhooks.py | 5 ++++ tests/test_webhook_verification.py | 39 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index 5aadff1..f9f1c2e 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -159,6 +159,11 @@ def _status_envelope(event: WebhookEvent) -> TaskStatus | None: given = needs_input if isinstance(needs_input, dict) else {} body = {"status": "needs_input", "task_id": raw.get("task_id") or ""} body.update({k: v for k, v in given.items() if v is not None}) + if isinstance(body.get("result"), dict): + # A key the payload left out reads as ``event.result`` reads it (the + # original payload's default: ``visibility`` "private", ``depth`` + # "standard", ``created_at`` "", ...), never the model's own default. + body["result"] = _original_result(body["result"]) try: return TaskStatus.model_validate(body) except ValidationError: diff --git a/tests/test_webhook_verification.py b/tests/test_webhook_verification.py index c43fb84..0009c0a 100644 --- a/tests/test_webhook_verification.py +++ b/tests/test_webhook_verification.py @@ -88,3 +88,42 @@ def test_other_events_do_not_carry_it() -> None: {"event": "something.new", "verification": {"status": "completed"}}, ): assert not hasattr(parse_webhook(payload), "verification") + + +_SPARSE_RESULT = {"verification_id": "v1", "claim": "A.", "verdict": "True", "lenz_score": 9} +_SPARSE = [ + pytest.param( + { + "event": "verification.completed", + "event_id": "evt_1", + "verification": {"status": "completed", "task_id": "t", "result": dict(_SPARSE_RESULT)}, + }, + id="current-shape", + ), + pytest.param( + {"event": "verification.completed", "task_id": "t", "result": dict(_SPARSE_RESULT)}, + id="original-shape", + ), +] + + +@pytest.mark.parametrize("payload", _SPARSE) +def test_a_sparse_result_reads_the_webhook_defaults(payload: dict) -> None: + # The values ``event.result`` gives a key the payload left out (and the + # Node SDK's ``event.verification.result``), not the model's own defaults. + event = parse_webhook(payload) + assert isinstance(event, VerificationCompleted) + assert event.verification is not None + result = event.verification.result + assert isinstance(result, Verification) + assert (result.visibility, result.depth, result.created_at) == ("private", "standard", "") + assert (result.confidence, result.language, result.verdict, result.lenz_score) == ("low", "en", "True", 9) + + +def test_a_sparse_current_shape_result_matches_event_result() -> None: + event = parse_webhook(_SPARSE[0].values[0]) + assert isinstance(event, VerificationCompleted) + assert event.verification is not None and event.verification.result is not None + result = event.verification.result + for key in ("verification_id", "claim", "visibility", "depth", "verdict", "confidence", "created_at", "language"): + assert getattr(result, key) == event.result[key], key From faece4912f501920e6705eed0f389084968044a9 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:41:45 +0300 Subject: [PATCH 38/67] feat: verify_batch and verify_batch_and_wait take any Sequence of claims A caller's list[dict[str, str]] now type-checks (a list is invariant; Sequence is not); nothing changes at run time. The examples drop the annotation they needed for it. Co-Authored-By: Claude Opus 5.5 --- examples/core/quickstart.py | 5 ++--- examples/core/verify_llm_output.py | 5 ++--- src/lenz_io/client.py | 8 ++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/examples/core/quickstart.py b/examples/core/quickstart.py index 5b422e6..ff2bc42 100644 --- a/examples/core/quickstart.py +++ b/examples/core/quickstart.py @@ -18,9 +18,8 @@ from __future__ import annotations import os -from typing import Any -from lenz_io import Lenz, VerifyBatchItem +from lenz_io import Lenz def main() -> None: @@ -49,7 +48,7 @@ def main() -> None: # 3. verify — escalate the low-confidence rows to the full multi-model panel # verify_batch_and_wait takes up to 20 claims a call: the first 20 here low = [row for row in quick if row.status == "completed" and row.confidence == "low"] - doubtful: list[VerifyBatchItem | dict[str, Any]] = [{"claim": row.claim} for row in low[:20]] + doubtful = [{"claim": row.claim} for row in low[:20]] # Fall back to the demo claim so the walkthrough always reaches steps 3 # and 4 even when every row came back confident. doubtful = doubtful or [{"claim": "Sharks don't get cancer"}] diff --git a/examples/core/verify_llm_output.py b/examples/core/verify_llm_output.py index 99db5fe..c8f929f 100644 --- a/examples/core/verify_llm_output.py +++ b/examples/core/verify_llm_output.py @@ -15,9 +15,8 @@ from __future__ import annotations import os -from typing import Any -from lenz_io import Lenz, VerifyBatchItem +from lenz_io import Lenz LLM_OUTPUT = """ The Eiffel Tower was completed in 1889 and stands 330 meters tall. @@ -56,7 +55,7 @@ def main() -> None: # via ``verification_url`` and you can skip the escalation. # verify_batch_and_wait takes up to 20 claims a call: the first 20 here low = [c for c in quick if c.status == "completed" and c.confidence == "low"] - doubtful: list[VerifyBatchItem | dict[str, Any]] = [{"claim": c.claim} for c in low[:20]] + doubtful = [{"claim": c.claim} for c in low[:20]] print(f"Escalating {len(doubtful)} low-confidence claims to full verification:\n") results = client.verify_batch_and_wait(claims=doubtful, timeout=180) if doubtful else [] for r in results: diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index e0f849a..e3ff191 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -83,7 +83,7 @@ import os import time import uuid -from collections.abc import Callable, Iterator +from collections.abc import Callable, Iterator, Sequence from contextlib import contextmanager from typing import Any, Literal, TypedDict, TypeVar, overload @@ -768,7 +768,7 @@ def verify( def verify_batch( self, *, - claims: list[VerifyBatchItem | dict[str, Any]], + claims: Sequence[VerifyBatchItem | dict[str, Any]], webhook_url: str = "", language: str = "", visibility: str = "", @@ -1132,7 +1132,7 @@ def wait( def verify_batch_and_wait( self, *, - claims: list[VerifyBatchItem | dict[str, Any]], + claims: Sequence[VerifyBatchItem | dict[str, Any]], webhook_url: str = "", language: str = "", visibility: str = "", @@ -1869,7 +1869,7 @@ def _verify_submit( def _verify_batch( self, *, - claims: list[VerifyBatchItem | dict[str, Any]], + claims: Sequence[VerifyBatchItem | dict[str, Any]], webhook_url: str = "", language: str = "", visibility: str = "", From 8af33ad0885416a68231a8412e2233198bc44851 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:42:57 +0300 Subject: [PATCH 39/67] fix: a verification.* event's .verification is None when its nested status is another kind A verification.completed carrying a failed run (or any status that is not the event's) is not presented as a completed verification, as in the Node SDK. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/webhooks.py | 16 +++++++++++++++- tests/test_webhook_verification.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index f9f1c2e..83ea414 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -124,16 +124,30 @@ def event_id(self) -> str: return value if isinstance(value, str) else "" +#: The run status each ``verification.*`` event reports. +_EVENT_STATUS = { + "verification.completed": "completed", + "verification.failed": "failed", + "verification.needs_input": "needs_input", +} + + def _status_envelope(event: WebhookEvent) -> TaskStatus | None: """The verification of a ``verification.*`` event as ``client.get_status`` returns it: the newer payload's ``verification``, else one built from the - original flat fields. ``None`` when the payload cannot be read as one.""" + original flat fields. ``None`` when the payload cannot be read as one, + or its status is not the event's (``completed`` / ``failed`` / + ``needs_input``).""" raw = event.raw nested = raw.get("verification") body: dict[str, Any] if isinstance(nested, dict): # A ``null`` reads as the field left out (``hint: null`` on a pause). body = {k: v for k, v in nested.items() if v is not None} + # Never presented as this event's kind when it says it is another + # (a ``verification.completed`` carrying a failed run). + if body.get("status") != _EVENT_STATUS.get(event.event): + return None elif event.event == "verification.completed": body = {"status": "completed", "task_id": raw.get("task_id") or ""} if raw.get("result") is not None: diff --git a/tests/test_webhook_verification.py b/tests/test_webhook_verification.py index 0009c0a..8ecec1d 100644 --- a/tests/test_webhook_verification.py +++ b/tests/test_webhook_verification.py @@ -127,3 +127,18 @@ def test_a_sparse_current_shape_result_matches_event_result() -> None: result = event.verification.result for key in ("verification_id", "claim", "visibility", "depth", "verdict", "confidence", "created_at", "language"): assert getattr(result, key) == event.result[key], key + + +@pytest.mark.parametrize( + ("event", "nested"), + [ + ("verification.completed", "failed"), + ("verification.failed", "completed"), + ("verification.needs_input", "processing"), + ("verification.completed", None), + ], +) +def test_a_nested_status_of_another_kind_is_not_presented_as_this_one(event: str, nested: str | None) -> None: + body: dict = {"task_id": "t"} if nested is None else {"status": nested, "task_id": "t"} + parsed = parse_webhook({"event": event, "event_id": "evt_1", "verification": body}) + assert parsed.verification is None # type: ignore[attr-defined] From 262b059dca96331ee0042ce4062d91730bc0ab31 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:45:04 +0300 Subject: [PATCH 40/67] fix: an answer whose body is not JSON carries the idempotency key, as the class 2.x raised A keyed call whose response cannot be decoded still raises the JSONDecodeError 2.x raised, now with exc.idempotency_key. Pins that the *_and_wait helpers keep going through the methods 2.x called (review, citecheck, wait), so an override is honoured and gets the call's key. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 12 ++++-- tests/test_idempotent_resend.py | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index e3ff191..0ec6e3f 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -625,16 +625,22 @@ def _call_key(idempotency_key: str | None, idempotency: bool) -> str | None: @contextmanager -def _carrying_key(key: str | None) -> Iterator[None]: +def _carrying_key(key: str | None, *, unreadable: bool = False) -> Iterator[None]: """Put ``key`` on any ``LenzError`` raised inside (``exc.idempotency_key``) that does not carry one yet: every error of a call that sent a key says - which, so a resend can reuse it.""" + which, so a resend can reuse it. With ``unreadable``, also on a + ``ValueError`` (an answer whose body is not JSON), whose class stays the + one 2.x raised.""" try: yield except LenzError as exc: if key and exc.idempotency_key is None: exc.idempotency_key = key raise + except ValueError as exc: + if unreadable and key and getattr(exc, "idempotency_key", None) is None: + exc.idempotency_key = key # type: ignore[attr-defined] + raise def _names_the_job(field: str) -> Callable[[Any], bool]: @@ -2019,7 +2025,7 @@ def _request( """One API call, with the retry ladder. Every ``LenzError`` it raises carries the ``Idempotency-Key`` it sent (``exc.idempotency_key``).""" key = (headers or {}).get("Idempotency-Key") or None - with _carrying_key(key): + with _carrying_key(key, unreadable=True): return self._send( method, path, diff --git a/tests/test_idempotent_resend.py b/tests/test_idempotent_resend.py index 98da813..eac69c1 100644 --- a/tests/test_idempotent_resend.py +++ b/tests/test_idempotent_resend.py @@ -252,3 +252,75 @@ def test_the_body_sent_again_is_byte_identical(client: Lenz, slept: list[float]) first, second = _bodies(route) assert first == second assert json.loads(first)["language"] == "de" + + +class TestAnAnswerThatCannotBeRead: + def test_invalid_json_keeps_its_2x_class_and_carries_the_key(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/verify").respond(200, content=b"not json") + with pytest.raises(json.JSONDecodeError) as ei: + client.verify("A.", idempotency_key="k-4") + assert ei.value.idempotency_key == "k-4" # type: ignore[attr-defined] + + def test_a_body_cut_off_mid_read_carries_the_key(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/assess").mock(side_effect=httpx.RemoteProtocolError("peer closed connection mid-body")) + with pytest.raises(LenzConnectionError) as ei: + client.assess("A.", idempotency_key="k-5") + assert ei.value.idempotency_key == "k-5" + + +class TestTheWaitHelpersGoThroughTheSameMethodsAs2x: + """A subclass or test double overriding the submit keeps working.""" + + def test_review_and_wait_calls_review_with_the_key(self, slept: list[float]) -> None: + seen: list[str | None] = [] + + class Recording(Lenz): + def review(self, text, **kw): # type: ignore[no-untyped-def, override] + seen.append(kw.get("idempotency_key")) + return super().review(text, **kw) + + done = {"review_id": "r1", "status": "completed", "issues": [], "failures": [], "claims": []} + with Recording(api_key="lenz_test") as client, respx.mock(base_url=BASE) as r: + submit = r.post("/review").respond(202, json={"review_id": "r1", "status": "queued"}) + r.get("/reviews/r1").respond(200, json=done) + client.review_and_wait("Draft.") + assert seen and seen[0] == submit.calls.last.request.headers["Idempotency-Key"] + + def test_citecheck_and_wait_calls_citecheck_with_the_key(self, slept: list[float]) -> None: + seen: list[str | None] = [] + + class Recording(Lenz): + def citecheck(self, text=None, **kw): # type: ignore[no-untyped-def, override] + seen.append(kw.get("idempotency_key")) + return super().citecheck(text, **kw) + + done = { + "citecheck_id": "c1", + "status": "completed", + "citations": [], + "citation_issues": [], + "citation_failures": [], + } + with Recording(api_key="lenz_test") as client, respx.mock(base_url=BASE) as r: + submit = r.post("/citecheck").respond(202, json={"citecheck_id": "c1", "status": "queued"}) + r.get("/citechecks/c1").respond(200, json=done) + client.citecheck_and_wait("Draft.", idempotency_key="k-6") + assert seen == ["k-6"] == [submit.calls.last.request.headers["Idempotency-Key"]] + + def test_verify_and_wait_waits_through_wait(self) -> None: + waited: list[str] = [] + + class Recording(Lenz): + def wait(self, task, **kw): # type: ignore[no-untyped-def, override] + waited.append(task.task_id) + return super().wait(task, **kw) + + with Recording(api_key="lenz_test") as client, respx.mock(base_url=BASE) as r: + r.post("/verify").respond(200, json=_ACCEPTED) + r.get("/verify/status/t1").respond( + 200, json={"status": "completed", "result": {"verification_id": "v1", "claim": "A."}} + ) + client.verify_and_wait("A.") + assert waited == ["t1"] From 9688e55f141c8045d6d7310a2207f74495ff2b45 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:46:05 +0300 Subject: [PATCH 41/67] docs: resend only with the same key, the 409 resend, the wait deadline and the batch outcomes README errors, retry and idempotency sections, the ask.send docstring and the CHANGELOG say a resend is safe only with exc.idempotency_key, that a 409 idempotency_conflict is sent again inside the call, how a wait treats its deadline and a batch item's 404 / version error / 401, and the iterator and transport-error changes. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 61 +++++++++++++++++++++++++++++++++++-------- README.md | 53 +++++++++++++++++++++++++------------ src/lenz_io/client.py | 8 ++++-- 3 files changed, 93 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf546a4..98436fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,21 +108,43 @@ Deprecated). and `extract` already did), so a retried batch or question replays the first answer instead of being charged twice. A key you pass wins; `idempotency=False` sends none. Asking the same question again is a new - call with a new key, so it is asked again. A retry that lands while the - first attempt still runs gets the 409 it got before; the SDK never sends a - second key to get past it. 2.x sent a key there only when you passed one. + call with a new key, so it is asked again. 2.x sent a key there only when + you passed one. +- **A 409 `idempotency_conflict` is sent again with the same key.** When a + call that sent an `Idempotency-Key` (its own or yours) meets the first + request with that key still running, it sends the same key and body again + after the wait the server states (at most 60s; a longer one falls back to + the usual backoff), within the call's retries. Still conflicting, it raises + the error 2.x raised (same class, `code` and message) with + `retryable=True`. It never mints a second key to get past it. A `review` / + `citecheck` conflict that names its job still returns that job at once. + 2.x raised the first 409. - **`wait`, `verify_and_wait` and `verify_batch_and_wait` stop at once on a 401, 403 or 404** (raising `LenzAuthError` / `LenzNotFoundError`; in a batch, that item is `failed` with no `status_detail` and the others keep going) instead of polling to a misleading `LenzTimeoutError`. A 5xx, a 429 - or a network failure is still polled again, after the wait it stated. Each - poll is one request bounded by what is left of the deadline (at least 1s, - at most the client timeout); the client's own retry ladder no longer runs - inside a poll. + or a network failure is still polled again, after the wait it stated. In + `verify_batch_and_wait` a 404, a 410 or an answer in another API version + fails that item only (`failed`, no `status_detail`), while a 401 / 403 + refuses the key itself and raises from the call; a single wait raises all + of them. Each poll is one request bounded by what is left of the deadline + and by the client's own timeout (`Lenz(timeout=)` as a number, `None` or + an `httpx.Timeout`, or an `http_client=`'s own); the client's own retry + ladder no longer runs inside a poll. +- **No poll starts once a wait's deadline is spent** (`wait`, + `verify_and_wait`, `verify_batch_and_wait`, `review_and_wait`, + `citecheck_and_wait`): the ids left are timed out. 2.x polled once more at + the deadline, past it. The first poll of a wait always runs, so + `timeout=0` still reads the status once (bounded by the client timeout). - **Network failures and transport timeouts raise subclasses of the class they raised before**: `LenzConnectionError` and `LenzRequestTimeoutError` (a `LenzConnectionError`), both `LenzAPIError`s, with the same message and - the `httpx` exception as `__cause__`. + the `httpx` exception as `__cause__`. Every `httpx.TransportError` worth + sending again (a server that hung up mid-response, a proxy failure, a read + or write error) is now retried and raised this way, and a wait polls again + after one; 2.x let these escape as the raw `httpx` exception. A request + that could never be sent (`httpx.UnsupportedProtocol`, + `httpx.LocalProtocolError`) still raises the `httpx` exception. - **A 404 raises `LenzNotFoundError`** (a `LenzError`, as before) and its `fix` reads "Check the id or key the call names: nothing with it is visible to this credential. Retrying will not help." (2.x advised @@ -135,13 +157,24 @@ Deprecated). `LenzRequestTimeoutError` (see Changed). - **`retryable` on every error**, set when the error is built: whether sending the same request again can succeed. `True` for a connection failure, a - request timeout, a 429 and a 5xx; `False` for any other 4xx and a + request timeout, a 429, a 5xx and a 409 `idempotency_conflict` or + `verification_not_ready` (read from the body as sent, so also where the 2.x + `code` is `""`); `False` for any other 4xx and a `LenzApiVersionError`; `None` when there was no HTTP status (a missing key, a `*_and_wait` timeout, a needs-input pause, a bad webhook signature). A failed verification, review or citation check keeps the server's value (`None` when it sent none), and a boolean `retryable` in a response's `failure` block, else at its top level, always wins. A `retryable=` passed to an error's constructor wins too. +- **`idempotency_key` on every error**: the `Idempotency-Key` the failed call + sent (yours or the automatic one), `None` when it sent none; set on every + error of that call, the wait of a `*_and_wait` helper included, and on the + `json.JSONDecodeError` an unreadable answer raises. A resend is safe only + with the same key: pass `idempotency_key=exc.idempotency_key` back and the + server replays the first answer instead of running it again. A plain new + call sends a new key and can run (and charge) the work twice. +- `verify_batch` and `verify_batch_and_wait` take any `Sequence` of claims + (a `list[dict[str, str]]` now type-checks); nothing changes at run time. - `ReviewFailedError`, `ReviewTimeoutError`, `CitecheckFailedError` and `CitecheckTimeoutError`: the job errors under the names the Node SDK uses (the same classes as `ReviewFailed`, `ReviewTimeout`, `CitecheckFailed`, @@ -149,13 +182,19 @@ Deprecated). - **`verifications.iter()` and `library.iter(**filters)`**: every item, page after page from `page`, fetched lazily (a page only when its first item is asked for), the page size read from each response, ending after a short or - empty page. `library.iter` takes `list`'s filters and refuses + empty page, once the pages read reach the response's `total`, when a + response states no positive `page_size`, or (without yielding it) when the + server answers another page than the one asked for. A start page below 1 + raises `ValueError`. `library.iter` takes `list`'s filters and refuses `sort="random"` (`ValueError`), which is not exhaustive. - **`.verification` on the `verification.completed`, `verification.failed` and `verification.needs_input` webhook events**: the verification as `client.get_status` returns it (a `TaskStatus`; on a completed event the verdict is `.verification.result`, a typed `Verification`), built from - either payload shape, `None` when a payload cannot be read as one. A + either payload shape, `None` when a payload cannot be read as one or its + nested status is not the event's. A key a sparse `result` leaves out reads + as `event.result` reads it (`visibility` `"private"`, `depth` `"standard"`, + `created_at` `""`, ...). A property, so the events' fields and `repr` are unchanged; the dict `result` stays (prefer `.verification`). A recognised event whose nested `result` is not an object no longer crashes `parse_webhook`. diff --git a/README.md b/README.md index 5093af6..12f5d6c 100644 --- a/README.md +++ b/README.md @@ -459,8 +459,10 @@ for r in results: ``` A `failed` item with `status_detail is None` is a verification its account's -retention period has removed (HTTP 410, see [Retention](#retention)); every -other failure carries a `status_detail`. +retention period has removed (HTTP 410, see [Retention](#retention)), a task +id nothing was found under (404) or an answer in another API version; every +other failure carries a `status_detail`. A 401 / 403 on a poll (the key +itself refused) raises from the whole call. A verify takes ~90 seconds, so show your users where it is. `on_progress` fires once per poll while the run is going — it takes the `task_id` as well, because @@ -821,18 +823,26 @@ except LenzNotFoundError as exc: except LenzConnectionError as exc: # No answer at all, after the SDK's own retries: the network, DNS, TLS, # or (LenzRequestTimeoutError, a subclass) one request's timeout. A - # LenzAPIError, as in 2.x; exc.__cause__ is the httpx exception. + # LenzAPIError, as in 2.x; exc.__cause__ is the httpx exception. The + # request may have reached the server: resend with the SAME key + # (idempotency_key=exc.idempotency_key), never as a plain new call. schedule_retry_in(30) ``` **`retryable`** (since 3.0, on every error): whether sending the same request -again can succeed. `True` for a connection failure, a request timeout, a 429 -and a 5xx; `False` for any other 4xx and a version error; `None` when there was no HTTP -status (a missing key, a `*_and_wait` timeout). A failed -verification, review or citation check carries the server's own value (`None` -when it sent none). Calls that charge send an `Idempotency-Key`, so resending -one the SDK raised for replays rather than runs twice when you pass the same -key. +again can succeed. `True` for a connection failure, a request timeout, a 429, +a 5xx and a 409 that means "not yet" (`idempotency_conflict`: the first +request with that key is still running; `verification_not_ready`); `False` +for any other 4xx and a version error; `None` when there was no HTTP status +(a missing key, a `*_and_wait` timeout). A failed verification, review or +citation check carries the server's own value (`None` when it sent none). + +**`idempotency_key`** (since 3.0, on every error): the `Idempotency-Key` the +failed call sent, or `None` when it sent none. A resend is safe only with the +same key: pass `idempotency_key=exc.idempotency_key` back, and the server +replays the first answer (or reports the first request still running) +instead of running it again. A plain new call sends a new key, and can run +(and charge) the work twice. ```python from lenz_io import LenzError @@ -841,6 +851,8 @@ try: client.assess(claim="...") except LenzError as exc: if exc.retryable: + # Later, the same request with the same key: + # client.assess(claim="...", idempotency_key=exc.idempotency_key) schedule_retry_in(getattr(exc, "retry_after", None) or 30) else: raise @@ -852,8 +864,12 @@ Local argument mistakes (an empty id, two exclusive arguments) raise A `*_and_wait` helper that reaches its own `timeout` raises `LenzTimeoutError` (`ReviewTimeout`, `CitecheckTimeout`): the job keeps running server-side, so read it later by its id rather than resubmitting. A poll answered 401, 403 or -404 ends the wait at once with that error; a 5xx, a 429 or a network failure -is polled again. `ReviewFailed`, `ReviewTimeout`, `CitecheckFailed` and +404 (or in another API version) ends the wait at once with that error; in +`verify_batch_and_wait` a 404 or a version error fails that item only (the +others keep going), while a 401 / 403 raises. A 5xx, a 429 or a network +failure is polled again. Each poll is bounded by what is left of the +`timeout` (at most the client timeout), and no poll starts once it is spent; +the first poll always runs, so `timeout=0` reads the status once. `ReviewFailed`, `ReviewTimeout`, `CitecheckFailed` and `CitecheckTimeout` are also importable as `ReviewFailedError`, `ReviewTimeoutError`, `CitecheckFailedError` and `CitecheckTimeoutError` (the same classes). @@ -947,9 +963,14 @@ reply = client.ask.send( ) ``` -A retry that arrives while the first call is still running gets a 409 -(`LenzError`) rather than the reply — there is nothing finished to replay -yet. +A retry that arrives while the first call with that key is still running is +answered 409 `idempotency_conflict`. The SDK sends the same key and body +again inside the same call, after the wait the server states (or its usual +backoff), within the call's retries; if the first call is still running +after them, it raises that `LenzError` with `retryable=True`. It never mints +a second key to get past it. Every error of a call that sent a key carries +it as `exc.idempotency_key`: resend with that key, never as a plain new call, +which would send a new key and could run (and charge) the work twice. ## Steering extract @@ -1081,7 +1102,7 @@ batch = client.verify_batch( Lenz( api_key="lenz_...", # or set LENZ_API_KEY env var base_url="https://lenz.io/api/v1", # override for staging / local - timeout=30.0, + timeout=30.0, # seconds per request; also None or an httpx.Timeout max_retries=3, ) ``` diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 0ec6e3f..0f7db79 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -463,8 +463,12 @@ def send( call's retries; pin your own with ``idempotency_key=`` (it wins) to make a retry from another process replay too, or pass ``idempotency=False`` to send none. A retry that arrives while the - first call is still running gets a 409: there is no reply to replay - yet. (Since 3.0; 2.x sent a key only when you passed one.) + first call is still running is answered 409 ``idempotency_conflict``: + the SDK sends the same key again within the call's retries, and if it + still conflicts raises it with ``retryable=True``. Every error of the + call carries the key (``exc.idempotency_key``): resend with it, never + as a plain new call, which would ask (and charge) again. (Since 3.0; + 2.x sent a key only when you passed one.) Never derived from the message: asking the same thing again is a normal thing to do here, and each turn also reads the history the From 0deb6c6e3eff507213601476c0a804f58882c600 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:52:44 +0300 Subject: [PATCH 42/67] fix: no batch item is polled past the deadline, and a poll keeps every configured timeout phase Only a timeout <= 0 wait reads each status once regardless of the deadline (as in 2.x); otherwise an item reached after an earlier poll spent the deadline is timed out, not polled (with timeout=None it could hang). Each poll keeps the client's connect, read, write and pool timeouts, each capped by what is left of the deadline, instead of one scalar for every phase. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 41 ++++++++++++++---------- tests/test_wait_stops.py | 69 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 0f7db79..7da52b6 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -1609,13 +1609,13 @@ def _wait_job( deadline = time.monotonic() + timeout last: _Job | None = None last_dump: dict[str, Any] | None = None - first = True + first = timeout <= 0 # ``timeout <= 0`` reads once, as in 2.x while True: # One request per poll, bounded by what is left of the deadline: # the client's own retry ladder inside a poll could run minutes # past it. A failed poll is retried on the next round instead. - # Once the deadline is spent no poll starts (the first one always - # runs, as in 2.x). + # Once the deadline is spent no poll starts (``timeout <= 0`` + # still reads once, as in 2.x). stated_wait: float | None = None job: _Job | None = None remaining = deadline - time.monotonic() @@ -1676,9 +1676,9 @@ def _poll_to_terminal( task has no ``TaskStatus`` — ``"timeout"`` is a client-side concept, never a wire status — so it lands in the second set, not the dict. - The first round polls every id once whatever the deadline (as in - 2.x, ``timeout=0`` reads each status once). After it no poll starts - once the deadline is spent: the ids left are timed out. Each poll is + No poll starts once the deadline is spent, not even within a round: + the ids left are timed out. The one exception is ``timeout <= 0``, + which reads every status once, as in 2.x. Each poll is ONE request whose timeout is what is left of the deadline, at most the client's own timeout (``_poll_timeout``): the client's own retry ladder inside a poll could run minutes past it, so a failed poll is @@ -1713,6 +1713,9 @@ def _poll_to_terminal( timed_out: set[str] = set() deadline = time.monotonic() + timeout backoff_idx = 0 + # ``timeout <= 0`` reads every id once, as in 2.x; otherwise no poll + # starts once the deadline is spent. + one_shot = timeout <= 0 first_round = True while pending: still_pending: list[str] = [] @@ -1720,7 +1723,7 @@ def _poll_to_terminal( stated_wait: float | None = None for task_id in pending: remaining = deadline - time.monotonic() - if remaining <= 0 and not first_round: + if remaining <= 0 and not (one_shot and first_round): # The deadline is spent: no poll starts past it. still_pending.append(task_id) continue @@ -1922,19 +1925,23 @@ def _timeout_at_least(self, floor: float) -> float | None: return None return floor - def _poll_timeout(self, remaining: float) -> float | None: - """The timeout of one poll request: what is left of the wait's - deadline, at most the client's own read timeout (``None`` there means - no client cap). Past the deadline (only the first poll of a wait runs - then) the client's own timeout, ``None`` meaning the client default. + def _poll_timeout(self, remaining: float) -> httpx.Timeout | None: + """The timeouts of one poll request: each phase (connect, read, write, + pool) the client in use configured, capped by what is left of the + wait's deadline (an unbounded phase gets just that). Past the deadline + (only a ``timeout <= 0`` wait polls then) ``None``: the client's own. Reads the client actually in use, so ``Lenz(timeout=None)``, an ``httpx.Timeout`` and an ``httpx.Client`` passed as ``http_client=`` all work.""" - cap = self._client.timeout.read if remaining <= 0: - return cap - return remaining if cap is None else min(remaining, cap) + return None + own = self._client.timeout + + def cap(phase: float | None) -> float: + return remaining if phase is None else min(phase, remaining) + + return httpx.Timeout(connect=cap(own.connect), read=cap(own.read), write=cap(own.write), pool=cap(own.pool)) def _extract( self, @@ -2022,7 +2029,7 @@ def _request( headers: dict[str, str] | None = None, auth_required: bool = True, auth_optional: bool = False, - timeout: float | None = None, + timeout: float | httpx.Timeout | None = None, max_retries: int | None = None, conflict_settles: Callable[[Any], bool] | None = None, ) -> dict[str, Any]: @@ -2053,7 +2060,7 @@ def _send( headers: dict[str, str] | None, auth_required: bool, auth_optional: bool, - timeout: float | None, + timeout: float | httpx.Timeout | None, max_retries: int | None, conflict_settles: Callable[[Any], bool] | None, ) -> dict[str, Any]: diff --git a/tests/test_wait_stops.py b/tests/test_wait_stops.py index d21b554..7c2b7ea 100644 --- a/tests/test_wait_stops.py +++ b/tests/test_wait_stops.py @@ -354,3 +354,72 @@ def test_an_injected_unbounded_http_client(self, slept: list[float]) -> None: poll = r.get("/verify/status/t").respond(200, json=_DONE) client.wait("t", timeout=300) assert _read_timeouts(poll) == [pytest.approx(300.0, abs=1.0)] + + +class TestTheDeadlineBoundsEveryItem: + def test_items_after_a_slow_first_poll_are_timed_out_not_polled(self, clock: list[float]) -> None: + polls: list[str] = [] + + def answer(request: httpx.Request) -> httpx.Response: + polls.append(request.url.path.rsplit("/", 1)[-1]) + clock[0] += 100.0 # every poll is slow + return httpx.Response(200, json=_RUNNING) + + with Lenz(api_key="lenz_test", timeout=None) as client, respx.mock(base_url=BASE) as r: + r.post("/verify/batch").respond( + 200, + json={"batch_id": "b", "items": [{"task_id": t, "claim": "A."} for t in ("a", "b", "c")]}, + ) + r.get(url__regex=r"/verify/status/.*").mock(side_effect=answer) + results = client.verify_batch_and_wait(claims=[{"claim": "A."}] * 3, timeout=10) + assert polls == ["a"] + assert [x.status for x in results] == ["timeout"] * 3 + + def test_a_zero_timeout_batch_reads_every_item_once_as_in_2x(self, client: Lenz, clock: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + r.post("/verify/batch").respond( + 200, json={"batch_id": "b", "items": [{"task_id": "a", "claim": "A."}, {"task_id": "t", "claim": "B."}]} + ) + a = r.get("/verify/status/a").respond(200, json=_RUNNING) + t = r.get("/verify/status/t").respond(200, json=_DONE) + results = client.verify_batch_and_wait(claims=[{"claim": "A."}, {"claim": "B."}], timeout=0) + assert (a.call_count, t.call_count) == (1, 1) + assert [x.status for x in results] == ["timeout", "completed"] + + +class TestEveryTimeoutPhaseIsKept: + def test_each_phase_capped_by_what_is_left(self, clock: list[float]) -> None: + configured = httpx.Timeout(30.0, connect=1.0, pool=0.25, write=2.0) + with Lenz(api_key="lenz_test", timeout=configured) as client, respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_RUNNING) + with pytest.raises(LenzTimeoutError): + client.wait("t", timeout=5) + phases = [c.request.extensions["timeout"] for c in poll.calls] + assert phases[0] == {"connect": 1.0, "read": 5.0, "write": 2.0, "pool": 0.25} + assert phases[1] == {"connect": 1.0, "read": 3.0, "write": 2.0, "pool": 0.25} + + def test_an_unbounded_phase_is_bounded_by_the_deadline(self, clock: list[float]) -> None: + with ( + Lenz(api_key="lenz_test", timeout=httpx.Timeout(None, connect=1.0)) as client, + respx.mock(base_url=BASE) as r, + ): + poll = r.get("/verify/status/t").respond(200, json=_DONE) + client.wait("t", timeout=5) + assert poll.calls.last.request.extensions["timeout"] == { + "connect": 1.0, + "read": 5.0, + "write": 5.0, + "pool": 5.0, + } + + def test_a_zero_timeout_poll_keeps_the_client_phases(self, clock: list[float]) -> None: + configured = httpx.Timeout(30.0, connect=1.0, pool=0.25, write=2.0) + with Lenz(api_key="lenz_test", timeout=configured) as client, respx.mock(base_url=BASE) as r: + poll = r.get("/verify/status/t").respond(200, json=_DONE) + client.wait("t", timeout=0) + assert poll.calls.last.request.extensions["timeout"] == { + "connect": 1.0, + "read": 30.0, + "write": 2.0, + "pool": 0.25, + } From 86f22f18d4c15b8402150cfbb57aadc7aeef2417 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:53:30 +0300 Subject: [PATCH 43/67] fix: a keyed call whose answer the model refuses carries the idempotency key Response validation of every keyed call (verify, verify_batch, extract, assess, select, ask.send, review, citecheck) runs inside the key-carrying context: a pydantic ValidationError keeps its class and gains exc.idempotency_key. Co-Authored-By: Claude Opus 5.5 --- src/lenz_io/client.py | 46 +++++++++++++++++++-------------- tests/test_idempotent_resend.py | 23 +++++++++++++++++ 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 7da52b6..c45a84c 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -486,13 +486,14 @@ def send( key = _call_key(idempotency_key, idempotency) if key: headers["Idempotency-Key"] = key - body = self._p._request( - "POST", - f"/ask/{verification_id}", - json=payload, - headers=headers, - ) - return AskReply.model_validate(body) + with _carrying_key(key, unreadable=True): + body = self._p._request( + "POST", + f"/ask/{verification_id}", + json=payload, + headers=headers, + ) + return AskReply.model_validate(body) def reset(self, verification_id: str) -> bool: self._p._request("DELETE", f"/ask/{verification_id}") @@ -1340,7 +1341,8 @@ def review( ): return ReviewStarted(review_id=review_id, status="queued") raise - return ReviewStarted.model_validate(body) + with _carrying_key(headers["Idempotency-Key"], unreadable=True): + return ReviewStarted.model_validate(body) @overload def get_review(self, review_id: str) -> ReviewFull: ... @@ -1483,7 +1485,8 @@ def citecheck( if exc.status_code == 409 and exc.code == "idempotency_conflict" and isinstance(existing, str) and existing: return CitecheckStarted(citecheck_id=existing, status="queued") raise - return CitecheckStarted.model_validate(body) + with _carrying_key(headers["Idempotency-Key"], unreadable=True): + return CitecheckStarted.model_validate(body) def get_citecheck(self, citecheck_id: str) -> Citecheck: """Read a citation check. Raises :class:`LenzGoneError` (410) once @@ -1876,8 +1879,9 @@ def _verify_submit( headers = {} if idempotency_key: headers["Idempotency-Key"] = idempotency_key - body = self._request("POST", "/verify", json=payload, headers=headers) - return TaskAccepted.model_validate(body) + with _carrying_key(idempotency_key, unreadable=True): + body = self._request("POST", "/verify", json=payload, headers=headers) + return TaskAccepted.model_validate(body) def _verify_batch( self, @@ -1908,8 +1912,9 @@ def _verify_batch( headers = {} if idempotency_key: headers["Idempotency-Key"] = idempotency_key - body = self._request("POST", "/verify/batch", json=payload, headers=headers) - return BatchAccepted.model_validate(body) + with _carrying_key(idempotency_key, unreadable=True): + body = self._request("POST", "/verify/batch", json=payload, headers=headers) + return BatchAccepted.model_validate(body) def _timeout_at_least(self, floor: float) -> float | None: """``floor`` seconds, or ``None`` (the client's own timeout) when that @@ -1969,8 +1974,9 @@ def _extract( headers = {} if idempotency_key: headers["Idempotency-Key"] = idempotency_key - body = self._request("POST", "/extract", json=payload, timeout=timeout, headers=headers) - return _extracted(body, locate=locate) + with _carrying_key(idempotency_key, unreadable=True): + body = self._request("POST", "/extract", json=payload, timeout=timeout, headers=headers) + return _extracted(body, locate=locate) def _assess( self, @@ -2003,15 +2009,17 @@ def _assess( headers = {} if idempotency_key: headers["Idempotency-Key"] = idempotency_key - body = self._request("POST", "/assess", json=payload, timeout=timeout, headers=headers) - return AssessResponse.model_validate(body) + with _carrying_key(idempotency_key, unreadable=True): + body = self._request("POST", "/assess", json=payload, timeout=timeout, headers=headers) + return AssessResponse.model_validate(body) def _select(self, task_id: str, *, texts: list[str], idempotency_key: str | None = None) -> BatchAccepted: headers = {} if idempotency_key: headers["Idempotency-Key"] = idempotency_key - body = self._request("POST", f"/verify/{task_id}/select", json={"texts": texts}, headers=headers) - return BatchAccepted.model_validate(body) + with _carrying_key(idempotency_key, unreadable=True): + body = self._request("POST", f"/verify/{task_id}/select", json={"texts": texts}, headers=headers) + return BatchAccepted.model_validate(body) def _get_status(self, task_id: str) -> TaskStatus: body = self._request("GET", f"/verify/status/{task_id}") diff --git a/tests/test_idempotent_resend.py b/tests/test_idempotent_resend.py index eac69c1..527635d 100644 --- a/tests/test_idempotent_resend.py +++ b/tests/test_idempotent_resend.py @@ -324,3 +324,26 @@ def wait(self, task, **kw): # type: ignore[no-untyped-def, override] ) client.verify_and_wait("A.") assert waited == ["t1"] + + +_UNREADABLE = [ + ("/ask/v1", {"content": None, "message_id": 5}, lambda c: c.ask.send("v1", message="Why?", idempotency_key="k")), + ("/verify", {"task_id": ["x"]}, lambda c: c.verify("A.", idempotency_key="k")), + ("/verify/batch", {"items": "x"}, lambda c: c.verify_batch(claims=[{"claim": "A."}], idempotency_key="k")), + ("/assess", {"claims": "x"}, lambda c: c.assess("A.", idempotency_key="k")), + ("/extract", {"status": ["x"]}, lambda c: c.extract(text="A.", idempotency_key="k")), + ("/verify/t0/select", {"items": "x"}, lambda c: c.select("t0", claims=["A."], idempotency_key="k")), + ("/review", {"review_id": ["x"]}, lambda c: c.review("Draft.", idempotency_key="k")), + ("/citecheck", {"citecheck_id": ["x"]}, lambda c: c.citecheck("Draft.", idempotency_key="k")), +] + + +@pytest.mark.parametrize(("path", "body", "call"), _UNREADABLE) +def test_an_answer_the_model_refuses_carries_the_key(client: Lenz, path, body, call) -> None: + from pydantic import ValidationError + + with respx.mock(base_url=BASE) as r: + r.post(path).respond(200, json=body) + with pytest.raises(ValidationError) as ei: + call(client) + assert ei.value.idempotency_key == "k" # type: ignore[attr-defined] From 1ef364733c663374b852a826be08de99013600ff Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 14:54:06 +0300 Subject: [PATCH 44/67] docs: the zero-timeout read, the kept timeout phases and the key on an unreadable answer Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 +++++--- README.md | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98436fb..7205e36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,8 +134,9 @@ Deprecated). - **No poll starts once a wait's deadline is spent** (`wait`, `verify_and_wait`, `verify_batch_and_wait`, `review_and_wait`, `citecheck_and_wait`): the ids left are timed out. 2.x polled once more at - the deadline, past it. The first poll of a wait always runs, so - `timeout=0` still reads the status once (bounded by the client timeout). + the deadline, past it. A `timeout=0` (or below) still reads each status + once, as in 2.x. Each poll keeps the client's connect, read, write and + pool timeouts, each capped by what is left of the deadline. - **Network failures and transport timeouts raise subclasses of the class they raised before**: `LenzConnectionError` and `LenzRequestTimeoutError` (a `LenzConnectionError`), both `LenzAPIError`s, with the same message and @@ -169,7 +170,8 @@ Deprecated). - **`idempotency_key` on every error**: the `Idempotency-Key` the failed call sent (yours or the automatic one), `None` when it sent none; set on every error of that call, the wait of a `*_and_wait` helper included, and on the - `json.JSONDecodeError` an unreadable answer raises. A resend is safe only + `json.JSONDecodeError` or pydantic `ValidationError` an unreadable answer + raises (their classes unchanged). A resend is safe only with the same key: pass `idempotency_key=exc.idempotency_key` back and the server replays the first answer instead of running it again. A plain new call sends a new key and can run (and charge) the work twice. diff --git a/README.md b/README.md index 12f5d6c..f837f4c 100644 --- a/README.md +++ b/README.md @@ -868,8 +868,8 @@ read it later by its id rather than resubmitting. A poll answered 401, 403 or `verify_batch_and_wait` a 404 or a version error fails that item only (the others keep going), while a 401 / 403 raises. A 5xx, a 429 or a network failure is polled again. Each poll is bounded by what is left of the -`timeout` (at most the client timeout), and no poll starts once it is spent; -the first poll always runs, so `timeout=0` reads the status once. `ReviewFailed`, `ReviewTimeout`, `CitecheckFailed` and +`timeout` (each phase at most the client's own), and no poll starts once it +is spent; `timeout=0` reads each status once, as in 2.x. `ReviewFailed`, `ReviewTimeout`, `CitecheckFailed` and `CitecheckTimeout` are also importable as `ReviewFailedError`, `ReviewTimeoutError`, `CitecheckFailedError` and `CitecheckTimeoutError` (the same classes). From b4fad7d8dcb3b7377d3af0bf121c06484e3e1a77 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 21:40:52 +0300 Subject: [PATCH 45/67] fix: a task cancelled elsewhere ends a wait with the failed error, and *.cancelled webhooks are typed In API version 2026-10-11 a verification, review or citation check stopped elsewhere (the website's Stop button, another process) is the status `cancelled`. The wait helpers stopped only on completed, needs_input and failed, so they polled such a task until their timeout. They now end on it with the error 2.x raised for the original shape (failure_class `cancelled`, retryable False); a batch item is a failed row; the plain getters return the cancelled status without raising. `verification.cancelled` (VerificationCancelled), `review.cancelled` and `citecheck.cancelled` parse as typed events; a cancellation of older work keeps arriving as *.failed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- src/lenz_io/__init__.py | 2 + src/lenz_io/cli/citecheck.py | 2 + src/lenz_io/cli/render.py | 6 +- src/lenz_io/cli/review.py | 2 + src/lenz_io/cli/verify.py | 6 +- src/lenz_io/client.py | 42 +- src/lenz_io/models.py | 28 +- src/lenz_io/webhooks.py | 58 ++- .../canonical/citecheck__get_cancelled.json | 146 +++++++ .../canonical/review__get_cancelled.json | 190 +++++++++ .../verify__status_cancelled_durable.json | 12 + .../verify__status_cancelled_live.json | 12 + .../webhook__citecheck_cancelled.json | 150 +++++++ .../canonical/webhook__review_cancelled.json | 166 ++++++++ .../webhook__verification_cancelled.json | 16 + .../expected/citecheck__get_cancelled.json | 286 +++++++++++++ .../expected/review__get_cancelled.json | 385 ++++++++++++++++++ .../verify__status_cancelled_durable.json | 61 +++ .../verify__status_cancelled_live.json | 61 +++ .../webhook__citecheck_cancelled.json | 154 +++++++ .../expected/webhook__review_cancelled.json | 173 ++++++++ .../webhook__verification_cancelled.json | 15 + .../legacy/citecheck__get_cancelled.json | 148 +++++++ .../parity/legacy/review__get_cancelled.json | 197 +++++++++ .../verify__status_cancelled_durable.json | 17 + .../legacy/verify__status_cancelled_live.json | 17 + .../legacy/webhook__citecheck_cancelled.json | 153 +++++++ .../legacy/webhook__review_cancelled.json | 172 ++++++++ .../webhook__verification_cancelled.json | 19 + tests/test_cancelled.py | 383 +++++++++++++++++ tests/test_parity.py | 38 ++ tests/test_webhook_verification.py | 4 +- 32 files changed, 3095 insertions(+), 26 deletions(-) create mode 100644 tests/fixtures/parity/canonical/citecheck__get_cancelled.json create mode 100644 tests/fixtures/parity/canonical/review__get_cancelled.json create mode 100644 tests/fixtures/parity/canonical/verify__status_cancelled_durable.json create mode 100644 tests/fixtures/parity/canonical/verify__status_cancelled_live.json create mode 100644 tests/fixtures/parity/canonical/webhook__citecheck_cancelled.json create mode 100644 tests/fixtures/parity/canonical/webhook__review_cancelled.json create mode 100644 tests/fixtures/parity/canonical/webhook__verification_cancelled.json create mode 100644 tests/fixtures/parity/expected/citecheck__get_cancelled.json create mode 100644 tests/fixtures/parity/expected/review__get_cancelled.json create mode 100644 tests/fixtures/parity/expected/verify__status_cancelled_durable.json create mode 100644 tests/fixtures/parity/expected/verify__status_cancelled_live.json create mode 100644 tests/fixtures/parity/expected/webhook__citecheck_cancelled.json create mode 100644 tests/fixtures/parity/expected/webhook__review_cancelled.json create mode 100644 tests/fixtures/parity/expected/webhook__verification_cancelled.json create mode 100644 tests/fixtures/parity/legacy/citecheck__get_cancelled.json create mode 100644 tests/fixtures/parity/legacy/review__get_cancelled.json create mode 100644 tests/fixtures/parity/legacy/verify__status_cancelled_durable.json create mode 100644 tests/fixtures/parity/legacy/verify__status_cancelled_live.json create mode 100644 tests/fixtures/parity/legacy/webhook__citecheck_cancelled.json create mode 100644 tests/fixtures/parity/legacy/webhook__review_cancelled.json create mode 100644 tests/fixtures/parity/legacy/webhook__verification_cancelled.json create mode 100644 tests/test_cancelled.py diff --git a/src/lenz_io/__init__.py b/src/lenz_io/__init__.py index eb820c0..bb66860 100644 --- a/src/lenz_io/__init__.py +++ b/src/lenz_io/__init__.py @@ -149,6 +149,7 @@ CitecheckEvent, LenzWebhooks, ReviewEvent, + VerificationCancelled, VerificationCompleted, VerificationFailed, VerificationNeedsInput, @@ -249,6 +250,7 @@ "Verdict", "VerdictLabel", "Verification", + "VerificationCancelled", "VerificationCompleted", "VerificationFailed", "VerificationList", diff --git a/src/lenz_io/cli/citecheck.py b/src/lenz_io/cli/citecheck.py index 4f561e6..71c63d3 100644 --- a/src/lenz_io/cli/citecheck.py +++ b/src/lenz_io/cli/citecheck.py @@ -238,6 +238,8 @@ def render_citecheck(out: Output, check: Citecheck) -> None: if check.failure is not None: hint = check.failure.hint or check.failure.code or "no reason given" out.console.print(f"[red]Failed:[/red] {escape(hint)}") + elif check.status == "cancelled": + out.console.print("[red]Cancelled.[/red]") render_citations(out, check) if check.status == "completed" and not check.citation_issues and not check.citation_failures: out.console.print("\n[green]No issues.[/green]") diff --git a/src/lenz_io/cli/render.py b/src/lenz_io/cli/render.py index 9fa191f..b288d4b 100644 --- a/src/lenz_io/cli/render.py +++ b/src/lenz_io/cli/render.py @@ -396,6 +396,8 @@ def render_task_status(out: Output, st: TaskStatus, *, task_id: str = "") -> Non # and `--detach` returns the spawned task_id(s) without blocking. Drop # both flags for the interactive picker. out.console.print(f"[dim]resolve it:[/dim] lenz verify --resume {ref} --claim --detach") + elif state == "cancelled": + out.console.print("[red]cancelled[/red]") elif state == "failed": err = st.error or st.failure_detail or st.failure_reason or "Verification failed." out.console.print(f"[red]failed[/red] [dim]— {err}[/dim]") @@ -428,6 +430,8 @@ def _batch_status_cell(st: Any) -> Any: return Text(f"{v.verdict or '?'} ({v.confidence}){score}", style=f"bold {color}") if st.status == "failed": return Text("failed", style="red") + if st.status == "cancelled": + return Text("cancelled", style="red") return Text(st.status or "?", style="yellow") @@ -481,7 +485,7 @@ def render_batch_details(out: Output, picks: list[tuple[str, str]], statuses: di st = statuses.get(tid) if st is not None and st.status == "completed" and st.result is not None: _batch_verdict_block(out, st.result) - elif st is not None and st.status == "failed": + elif st is not None and st.status in ("failed", "cancelled"): out.console.print(f"[red]Failed:[/red] {st.error or st.failure_detail or 'pipeline error'}") else: label = "timed out" if st is None else (st.status or "unknown") diff --git a/src/lenz_io/cli/review.py b/src/lenz_io/cli/review.py index ec96d41..a616595 100644 --- a/src/lenz_io/cli/review.py +++ b/src/lenz_io/cli/review.py @@ -433,6 +433,8 @@ def render_review(out: Output, review: ReviewFull, *, issues_only: bool = False) if review.failure is not None: hint = review.failure.hint or review.failure.code or "no reason given" out.console.print(f"[red]Failed:[/red] {escape(hint)}") + elif review.status == "cancelled": + out.console.print("[red]Cancelled.[/red]") n = review.summary.claims_selected or len(review.claims) if issues_only: for i in review.issues: diff --git a/src/lenz_io/cli/verify.py b/src/lenz_io/cli/verify.py index fabdbcc..aa613f7 100644 --- a/src/lenz_io/cli/verify.py +++ b/src/lenz_io/cli/verify.py @@ -188,7 +188,7 @@ def _poll( if st.status == "completed": render_verification(out, st.result) return - if st.status == "failed": + if st.status in ("failed", "cancelled"): # cancelled elsewhere: the same exit as a failure raise CLIError(st.error or st.failure_detail or "Verification failed.", code="pipeline_failed") if st.status == "needs_input": if st.reason == "multi_claim": @@ -371,7 +371,7 @@ def _batch_item_json(task_id: str, claim_text: str, st: TaskStatus | None) -> di "status": "completed", "verification": st.result.model_dump(mode="json"), } - if st.status == "failed": + if st.status in ("failed", "cancelled"): # cancelled elsewhere: a failed row, as in the original shape return {"task_id": task_id, "claim": claim_text, "status": "failed", "error": st.error or st.failure_detail} return {"task_id": task_id, "claim": claim_text, "status": st.status or "unknown"} @@ -419,7 +419,7 @@ def _resume( raise if st.status == "completed": render_verification(out, st.result) - elif st.status == "failed": + elif st.status in ("failed", "cancelled"): raise CLIError(st.error or st.failure_detail or "Verification failed.", code="pipeline_failed") else: # processing / needs_input → keep polling from here, honoring --claim/--detach _poll(client, out, ident, timeout, selection=selection, detach=detach) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index c45a84c..53916a4 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -122,6 +122,7 @@ Citecheck, CitecheckStarted, ExtractedClaims, + FailureBlock, LibraryItem, LibraryList, Progress, @@ -182,6 +183,10 @@ RETRY_BACKOFF = (1.0, 2.0, 4.0) POLL_BACKOFF = (2.0, 4.0, 8.0) POLL_BACKOFF_CAP = 10.0 +# The statuses a verification poll ends on. ``cancelled`` is a task stopped +# elsewhere (the website's Stop button, another process): its own status in +# API version 2026-10-11, where 2026-05-13 said ``failed``. +_TERMINAL_STATUSES = ("completed", "needs_input", "failed", "cancelled") # Bounds on the server's ``progress.poll_after_seconds``. A hint outside # them is treated as garbage and the local ladder is used instead — the # floor stops a bad value turning the poll loop into a hot loop, the @@ -1113,7 +1118,10 @@ def wait( ``verify`` / ``select`` — so ``client.wait(client.verify(claim=...))`` reads naturally. Raises ``ValueError`` for an empty id, ``LenzNeedsInputError`` / ``LenzPipelineError`` on terminal - non-success, ``LenzGoneError`` if its account's retention period has + non-success (a verification cancelled elsewhere, such as the + website's Stop button, raises the same ``LenzPipelineError`` as a + failed one, with ``failure_class == "cancelled"``), + ``LenzGoneError`` if its account's retention period has removed the verification, and ``LenzTimeoutError`` if ``timeout`` (default ``WAIT_TIMEOUT``, 300s) elapses (the task may still finish server-side — resume via ``get_status``). @@ -1652,7 +1660,7 @@ def _wait_job( last = job if job.status == "completed": return job - if job.status == "failed": + if job.status in ("failed", "cancelled"): raise failed(job) remaining = deadline - time.monotonic() if remaining <= 0: @@ -1673,7 +1681,7 @@ def _poll_to_terminal( on_progress: Callable[[str, Progress], None] | None = None, ) -> tuple[dict[str, TaskStatus], set[str], dict[str, LenzError]]: """Round-robin poll ``task_ids`` until each reaches a terminal state - (completed / needs_input / failed) or the deadline elapses. + (completed / needs_input / failed / cancelled) or the deadline elapses. Returns ``(terminal_by_id, timed_out_ids, stopped_by_id)``. A timed-out task has no ``TaskStatus`` — ``"timeout"`` is a client-side concept, @@ -1758,7 +1766,7 @@ def _poll_to_terminal( stated_wait = wait_s if stated_wait is None else max(stated_wait, wait_s) still_pending.append(task_id) continue - if status.status in ("completed", "needs_input", "failed"): + if status.status in _TERMINAL_STATUSES: terminal[task_id] = status else: still_pending.append(task_id) @@ -1816,7 +1824,9 @@ def _verification_from_terminal(self, status: TaskStatus, task_id: str) -> Verif hint=status.hint, payload=status.model_dump(), ) - # failed. ``error`` is the 2.x sentence, rebuilt from ``failure``. + # failed, or cancelled elsewhere (the same outcome, as in 2.x, where the + # original shape said ``failed`` with ``failure_class`` ``cancelled``). + # ``error`` is the 2.x sentence, rebuilt from ``failure``. detail = status.error or status.failure_detail or status.failure_reason or "unknown" if status.retryable: fix = "Transient provider outage — retry the same request after a short wait." @@ -2227,8 +2237,26 @@ def _is_citecheck_body(body: Any, citecheck_id: str) -> bool: ) +#: What the original response shape said of a task cancelled elsewhere, which +#: the current one states as the status ``cancelled`` and no failure block. +_CANCELLED_FAILURE = { + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": False, + "docs_url": "https://lenz.io/docs/errors#cancelled", +} + + +def _failure_of(job: Citecheck | ReviewFull) -> FailureBlock | None: + """Why a job ended without a result: its failure block, or the cancelled + one for a job cancelled elsewhere.""" + if job.failure is None and job.status == "cancelled": + return FailureBlock.model_validate(_CANCELLED_FAILURE) + return job.failure + + def _citecheck_failed(check: Citecheck) -> CitecheckFailed: - failure = check.failure + failure = _failure_of(check) reason = (failure.failure_reason if failure else None) or "" hint = (failure.hint if failure else None) or "" retryable = failure.retryable if failure is not None and isinstance(failure.retryable, bool) else None @@ -2248,7 +2276,7 @@ def _citecheck_failed(check: Citecheck) -> CitecheckFailed: def _review_failed(review: ReviewFull) -> ReviewFailed: - failure = review.failure + failure = _failure_of(review) reason = (failure.failure_reason if failure else None) or "" hint = (failure.hint if failure else None) or "" retryable = failure.retryable if failure is not None and isinstance(failure.retryable, bool) else None diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 5e31386..8474f74 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -968,7 +968,11 @@ def _original_status_error(code: Any, detail: Any) -> Any: class TaskStatus(_Lax): """Returned by ``GET /verify/status/{task_id}``.""" - status: str = "" # processing | needs_input | completed | failed + # processing | needs_input | completed | failed | cancelled. ``cancelled`` + # is a task stopped elsewhere (the website's Stop button, another + # process): its own status since API version 2026-10-11, which the + # original shape said as ``failed`` with ``failure_class`` ``cancelled``. + status: str = "" # Echoed on every shape since 2026-09, so a caller polling several # verifications in one loop can tell the replies apart. ``""`` from # older servers. @@ -1015,6 +1019,17 @@ class TaskStatus(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: + if isinstance(data, dict) and data.get("status") == "cancelled" and "failure" not in data: + # A task cancelled elsewhere: its own status, with no failure + # block. The 2.x fields read what the original shape said of it. + return _fill( + data, + error=_STATUS_ERRORS["cancelled"], + failure_reason="cancelled", + failure_class="cancelled", + retryable=False, + docs_url="https://lenz.io/docs/errors#cancelled", + ) if not _is_newer(data, "failure") or not isinstance(data["failure"], dict): return data failure = data["failure"] @@ -1057,7 +1072,8 @@ class BatchItemResult(_Lax): - ``completed`` — ``verification`` is set (and ``status_detail`` carries the raw poll). - ``needs_input`` — paused for caller input; inspect ``status_detail`` (reason / claims). - - ``failed`` — terminal failure (or completed-without-result); ``status_detail`` carries the diagnostic. + - ``failed`` — terminal failure (or completed-without-result), or cancelled elsewhere + (``status_detail.status`` is then ``cancelled``); ``status_detail`` carries the diagnostic. A verification removed by its account's retention period (HTTP 410) is ``failed``, ``status_detail`` ``None``. - ``timeout`` — the deadline elapsed before this task reached a terminal state; ``status_detail`` is ``None``. """ @@ -2031,7 +2047,9 @@ def citation_limit_exceeded(self) -> bool | None: class Citecheck(_Lax): """``GET /citechecks/{citecheck_id}``: a citation check as it stands. - ``status``: ``queued`` → ``checking`` → ``completed``, or ``failed``. + ``status``: ``queued`` → ``checking`` → ``completed``, or ``failed``, or + ``cancelled`` (stopped elsewhere; ``citecheck_and_wait`` raises the failed + error for it, ``failure_class`` ``cancelled``). ``outcome`` is ``None`` until it ends, then ``clean``, ``issues_found``, ``incomplete`` (a citation could not be checked for a reason of ours) or ``unchecked``. ``citations``, ``citation_issues`` and ``citation_failures`` @@ -2059,7 +2077,9 @@ class ReviewEnvelope(_Lax): """What both views of ``GET /reviews/{review_id}`` carry. ``status`` is the lifecycle: ``queued`` → ``assessing`` → ``verifying`` - → ``completed``, or ``failed``. ``outcome`` is ``None`` until the review + → ``completed``, or ``failed``, or ``cancelled`` (stopped elsewhere; + ``review_and_wait`` raises the failed error for it, ``failure_class`` + ``cancelled``). ``outcome`` is ``None`` until the review ends, then one of ``clean`` (every selected claim checked, no issue), ``issues_found``, ``incomplete`` (some work failed) or ``unchecked`` (it failed before any claim was checked; ``failure`` says why). diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index 83ea414..516db88 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -128,6 +128,7 @@ def event_id(self) -> str: _EVENT_STATUS = { "verification.completed": "completed", "verification.failed": "failed", + "verification.cancelled": "cancelled", "verification.needs_input": "needs_input", } @@ -137,7 +138,7 @@ def _status_envelope(event: WebhookEvent) -> TaskStatus | None: returns it: the newer payload's ``verification``, else one built from the original flat fields. ``None`` when the payload cannot be read as one, or its status is not the event's (``completed`` / ``failed`` / - ``needs_input``).""" + ``cancelled`` / ``needs_input``).""" raw = event.raw nested = raw.get("verification") body: dict[str, Any] @@ -245,6 +246,26 @@ def failure(self) -> FailureBlock | None: return None +@dataclass +class VerificationCancelled(WebhookEvent): + """``event=verification.cancelled`` — the verification was cancelled (from + the website, or by another process) before it finished. + + Sent only for work submitted with API version 2026-10-11 (this SDK's); + work submitted by an older client reports a cancellation as + ``verification.failed`` with ``failure_class`` ``cancelled`` + (:class:`VerificationFailed`). Nothing was charged. Read + :attr:`verification`: ``status`` is ``"cancelled"``. + """ + + @property + def verification(self) -> TaskStatus | None: + """The verification as ``client.get_status`` returns it: ``status`` + (``"cancelled"``) and ``task_id``. ``None`` when the payload cannot be + read as one.""" + return _status_envelope(self) + + @dataclass class VerificationNeedsInput(WebhookEvent): """``event=verification.needs_input`` — pipeline paused for caller input. @@ -311,12 +332,15 @@ class CertificateTimestamped(WebhookEvent): @dataclass class ReviewEvent(WebhookEvent): - """``event=review.completed`` or ``review.failed`` — a review ended. + """``event=review.completed``, ``review.failed`` or ``review.cancelled`` — + a review ended. ``review`` is the final review (the same body ``get_review`` returns), or ``None`` if it could not be parsed (``raw["review"]`` still has it). - ``status`` is ``completed`` or ``failed``; read ``review.outcome`` and - ``review.issues``. + ``status`` is ``completed``, ``failed`` or ``cancelled``; read + ``review.outcome`` and ``review.issues``. ``review.cancelled`` is sent + only for work submitted with API version 2026-10-11; an older client's + cancelled review arrives as ``review.failed``. Deduplicate on ``event_id``: it is the same on every delivery attempt of one event, while ``attempt`` counts up. ``task_id`` identifies the @@ -332,8 +356,10 @@ class ReviewEvent(WebhookEvent): @dataclass class CitecheckEvent(WebhookEvent): - """``event=citecheck.completed`` or ``citecheck.failed`` — a citation - check ended. + """``event=citecheck.completed``, ``citecheck.failed`` or + ``citecheck.cancelled`` — a citation check ended. ``citecheck.cancelled`` + is sent only for work submitted with API version 2026-10-11; an older + client's cancelled check arrives as ``citecheck.failed``. ``citecheck`` is the final check (the same body ``get_citecheck`` returns), or ``None`` if it could not be parsed (``raw["citecheck"]`` @@ -474,6 +500,17 @@ def _build_event(raw: dict[str, Any]) -> WebhookEvent: failure_class=str(payload.get("failure_class") or ""), retryable=payload.get("retryable") if isinstance(payload.get("retryable"), bool) else None, ) + if event == "verification.cancelled": + return VerificationCancelled( + event=event, + task_id=task_id, + attempt=attempt, + delivered_at=delivered_at, + verification_id=verification_id, + batch_id=batch_id, + status=status, + raw=raw, + ) if event == "verification.needs_input": needs_input = payload.get("needs_input") or {} return VerificationNeedsInput( @@ -500,7 +537,7 @@ def _build_event(raw: dict[str, Any]) -> WebhookEvent: raw=raw, coverage=payload.get("coverage") or {}, ) - if event in ("review.completed", "review.failed"): + if event in ("review.completed", "review.failed", "review.cancelled"): body = payload.get("review") try: review = ReviewFull.model_validate(body) if isinstance(body, dict) else None @@ -521,7 +558,7 @@ def _build_event(raw: dict[str, Any]) -> WebhookEvent: review_id=str(payload.get("review_id") or ""), review=review, ) - if event in ("citecheck.completed", "citecheck.failed"): + if event in ("citecheck.completed", "citecheck.failed", "citecheck.cancelled"): body = payload.get("citecheck") try: check = Citecheck.model_validate(body) if isinstance(body, dict) else None @@ -563,8 +600,8 @@ def parse_webhook(body: bytes | str | dict[str, Any]) -> WebhookEvent: use :meth:`LenzWebhooks.parse`, which checks the signature and the replay window first. - Returns a :class:`ReviewEvent` for ``review.*``, a :class:`CitecheckEvent` - for ``citecheck.*``, the matching verification + Returns a :class:`ReviewEvent` for ``review.*`` (``completed``, ``failed``, + ``cancelled``), a :class:`CitecheckEvent` for ``citecheck.*``, the matching verification or certificate event otherwise, and a plain :class:`WebhookEvent` for an event type this release does not know. Branch with ``isinstance`` and ignore events you do not handle. @@ -690,6 +727,7 @@ def _check_replay(self, payload: dict[str, Any]) -> None: "CitecheckEvent", "LenzWebhooks", "ReviewEvent", + "VerificationCancelled", "VerificationCompleted", "VerificationFailed", "VerificationNeedsInput", diff --git a/tests/fixtures/parity/canonical/citecheck__get_cancelled.json b/tests/fixtures/parity/canonical/citecheck__get_cancelled.json new file mode 100644 index 0000000..320730c --- /dev/null +++ b/tests/fixtures/parity/canonical/citecheck__get_cancelled.json @@ -0,0 +1,146 @@ +{ + "status": 200, + "body": { + "citecheck_id": "8eb49302", + "status": "cancelled", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 2 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } + }, + { + "citation_index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } + } + ], + "failure": null, + "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/review__get_cancelled.json b/tests/fixtures/parity/canonical/review__get_cancelled.json new file mode 100644 index 0000000..9ec4e20 --- /dev/null +++ b/tests/fixtures/parity/canonical/review__get_cancelled.json @@ -0,0 +1,190 @@ +{ + "status": 200, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "cancelled", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 0, + "failed": 1 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": null, + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "completed_at": null, + "verification_url": null, + "url": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": null, + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "status": "failed", + "content_status": "available" + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_cancelled_durable.json b/tests/fixtures/parity/canonical/verify__status_cancelled_durable.json new file mode 100644 index 0000000..2699174 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_cancelled_durable.json @@ -0,0 +1,12 @@ +{ + "status": 200, + "body": { + "status": "cancelled", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/verify__status_cancelled_live.json b/tests/fixtures/parity/canonical/verify__status_cancelled_live.json new file mode 100644 index 0000000..2699174 --- /dev/null +++ b/tests/fixtures/parity/canonical/verify__status_cancelled_live.json @@ -0,0 +1,12 @@ +{ + "status": 200, + "body": { + "status": "cancelled", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/canonical/webhook__citecheck_cancelled.json b/tests/fixtures/parity/canonical/webhook__citecheck_cancelled.json new file mode 100644 index 0000000..78dc010 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__citecheck_cancelled.json @@ -0,0 +1,150 @@ +{ + "status": 200, + "body": { + "event": "citecheck.cancelled", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "citecheck_id": "8eb49302", + "status": "cancelled", + "citecheck": { + "citecheck_id": "8eb49302", + "status": "cancelled", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 2 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } + }, + { + "citation_index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } + } + ], + "failure": null, + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/canonical/webhook__review_cancelled.json b/tests/fixtures/parity/canonical/webhook__review_cancelled.json new file mode 100644 index 0000000..4a440a8 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__review_cancelled.json @@ -0,0 +1,166 @@ +{ + "status": 200, + "body": { + "event": "review.cancelled", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "d6b2bd72", + "status": "cancelled", + "review": { + "review_id": "d6b2bd72", + "view": "full", + "status": "cancelled", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 0, + "failed": 1 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": null, + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "completed_at": null, + "verification_url": null, + "url": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": null, + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "status": "failed", + "content_status": "available" + }, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/canonical/webhook__verification_cancelled.json b/tests/fixtures/parity/canonical/webhook__verification_cancelled.json new file mode 100644 index 0000000..89f5704 --- /dev/null +++ b/tests/fixtures/parity/canonical/webhook__verification_cancelled.json @@ -0,0 +1,16 @@ +{ + "status": 200, + "body": { + "event": "verification.cancelled", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "cancelled", + "verification": { + "status": "cancelled", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/expected/citecheck__get_cancelled.json b/tests/fixtures/parity/expected/citecheck__get_cancelled.json new file mode 100644 index 0000000..095ba90 --- /dev/null +++ b/tests/fixtures/parity/expected/citecheck__get_cancelled.json @@ -0,0 +1,286 @@ +{ + "dump": { + "citation_failures": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "reference": "https://example.gov/report-2024" + }, + { + "citation_index": 1, + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "reference": "https://example.org/statement" + } + ], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "hint": "This source could not be checked this time. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": null, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "hint": "This source could not be checked this time. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": null, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "language": "en", + "more_citations": [], + "outcome": "incomplete", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "failed", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 2, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "dump_unset": { + "citation_failures": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "reference": "https://example.gov/report-2024" + }, + { + "citation_index": 1, + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "reference": "https://example.org/statement" + } + ], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "hint": "This source could not be checked this time. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": null, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "hint": "This source could not be checked this time. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": null, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "language": "en", + "more_citations": [], + "outcome": "incomplete", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "failed", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 2, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "exit_code": 2, + "render": "Citation check 8eb49302: incomplete — 0 credits charged\nFailed: cancelled\n\n2 sources cited in your draft\n2 could not be checked.\n\n[source 1/2] Could not be checked this time.\n https://example.gov/report-2024\n This source could not be checked this time. Try again later.\n\n[source 2/2] Could not be checked this time.\n https://example.org/statement\n This source could not be checked this time. Try again later.\n", + "render_json": null, + "repr": "Citecheck(citecheck_id='8eb49302', status='failed', outcome='incomplete', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', poll_after_seconds=None, policy=CitecheckPolicy(max_citations=2), summary=CitecheckSummary(citations_found=2, citations_selected=2, citation_limit=2, citation_limit_reached=False, citation_checks=ReviewCitationCheckCounts(checked=0, unchecked=0, failed=2), citation_issues=0), credits=ReviewCredits(charged=0), citations=[ReviewCitation(index=0, reference='https://example.gov/report-2024', cited_url='https://example.gov/report-2024', doi=None, statement='Unemployment fell to 4.1% in 2024.', quotes=[], position=None, result=None, check=ReviewCitationCheck(status='failed', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint='This source could not be checked this time. Try again later.', failure=FailureBlock(failure_reason='cancelled', failure_class='cancelled', retryable=False, hint='This source could not be checked this time. Try again later.', docs_url='https://lenz.io/docs/errors#cancelled'))), ReviewCitation(index=1, reference='https://example.org/statement', cited_url='https://example.org/statement', doi=None, statement='The agency said so in a statement.', quotes=[], position=None, result=None, check=ReviewCitationCheck(status='failed', page_read=None, page_title=None, page_published_date=None, page_language=None, source_url=None, source_version=None, support=None, snippet=None, rationale=None, quote=None, missing_quote=None, doi_registered=None, metadata=None, metadata_differences=[], registered=None, unchecked_reason=None, hint='This source could not be checked this time. Try again later.', failure=FailureBlock(failure_reason='cancelled', failure_class='cancelled', retryable=False, hint='This source could not be checked this time. Try again later.', docs_url='https://lenz.io/docs/errors#cancelled')))], citation_issues=[], citation_failures=[ReviewCitationFailure(citation_index=0, reference='https://example.gov/report-2024', cited_url='https://example.gov/report-2024', doi=None, failure=FailureBlock(failure_reason='cancelled', failure_class='cancelled', retryable=False, hint='This source could not be checked this time. Try again later.', docs_url='https://lenz.io/docs/errors#cancelled')), ReviewCitationFailure(citation_index=1, reference='https://example.org/statement', cited_url='https://example.org/statement', doi=None, failure=FailureBlock(failure_reason='cancelled', failure_class='cancelled', retryable=False, hint='This source could not be checked this time. Try again later.', docs_url='https://lenz.io/docs/errors#cancelled'))], more_citations=[], failure=FailureBlock(failure_reason='cancelled', failure_class='cancelled', retryable=False, hint=None, docs_url='https://lenz.io/docs/errors#cancelled'), language='en')" +} diff --git a/tests/fixtures/parity/expected/review__get_cancelled.json b/tests/fixtures/parity/expected/review__get_cancelled.json new file mode 100644 index 0000000..4694bbd --- /dev/null +++ b/tests/fixtures/parity/expected/review__get_cancelled.json @@ -0,0 +1,385 @@ +{ + "dump": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": null, + "confidence": null, + "content_status": "available", + "created_at": null, + "depth": null, + "domain": null, + "entities": [], + "executive_summary": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "key_finding": null, + "language": null, + "lenz_score": null, + "modified_at": null, + "status": "failed", + "suggested_rewrite": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": null, + "verdict": null, + "verification_id": null, + "verification_url": null, + "visibility": null, + "warnings": [] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Paris is the capital of France.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "failures": [], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 0, + "failed": 1, + "planned": 1 + } + }, + "view": "full" + }, + "dump_unset": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The EU AI Act took effect in March 2024.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": null, + "confidence": null, + "content_status": "available", + "created_at": null, + "depth": null, + "domain": null, + "entities": [], + "executive_summary": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "key_finding": null, + "language": null, + "lenz_score": null, + "modified_at": null, + "status": "failed", + "suggested_rewrite": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": null, + "verdict": null, + "verification_id": null, + "verification_url": null, + "visibility": null, + "warnings": [] + } + }, + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "True", + "verification_url": null + }, + "claim": "Paris is the capital of France.", + "escalation": { + "disposition": "not_selected", + "matched_rules": [] + }, + "index": 1, + "positions": null, + "result": { + "confidence": "high", + "is_issue": false, + "source": "assessment", + "verdict": "True" + }, + "suggested_edits": null, + "verification": null + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 2 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "failures": [], + "issues": [ + { + "claim": "The EU AI Act took effect in March 2024.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": { + "completed": 2, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 2, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 0, + "failed": 1, + "planned": 1 + } + }, + "view": "full" + }, + "exit_code": 2, + "render": "Review d6b2bd72: incomplete — 2 claims: 1 issue, 1 true or mostly true · 0 of 1 deep-checked · 2 credits charged\nFailed: cancelled\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n Deep check failed: The deep check failed.\n\n[2/2] Paris is the capital of France.\n True (high) · quick check\n", + "render_issues": "Review d6b2bd72: incomplete — 2 claims: 1 issue, 1 true or mostly true · 0 of 1 deep-checked · 2 credits charged\nFailed: cancelled\n\n[1/2] The EU AI Act took effect in March 2024.\n False (high) · quick check\n Deep check failed: cancelled\n", + "render_json": null, + "repr": "ReviewFull(review_id='d6b2bd72', view='full', status='failed', outcome='incomplete', created_at='2026-10-01T09:07:00.123456Z', completed_at='2026-10-01T10:07:00.123456Z', language='en', policy=EscalationPolicy(verdicts=['False', 'Mostly False', 'Mixed'], confidence=['low'], max_assessments=20, max_verifications=5, depth='standard', max_citations=0, suggest_edits=False), summary=ReviewSummary(claims_selected=2, claim_limit=20, claim_limit_reached=False, input_truncated=False, assessments=ReviewAssessmentCounts(completed=2, failed=0), verifications=ReviewVerificationCounts(planned=1, completed=0, failed=1), issues=1, citations_found=None, citations_selected=None, citation_limit=None, citation_limit_reached=None, citation_checks=None, citation_issues=0, citations_skipped=None), credits=ReviewCredits(charged=2), poll_after_seconds=None, issues=[ReviewIssue(claim_index=0, claim='The EU AI Act took effect in March 2024.', verified_claim=None, verdict='False', confidence='high', source='assessment', verification_id=None, verification_status='failed', verification_url=None, url=None, escalation=Escalation(matched_rules=['verdict'], disposition='planned'), key_finding=None, rationale=None, suggested_rewrite=None, failure=FailureBlock(failure_reason='cancelled', failure_class='cancelled', retryable=False, hint=None, docs_url='https://lenz.io/docs/errors#cancelled'), suggested_edits=None)], failures=[], citation_issues=[], citation_failures=[], more_claims=[], more_claim_locations=[], more_citations=[], failure=FailureBlock(failure_reason='cancelled', failure_class='cancelled', retryable=False, hint=None, docs_url='https://lenz.io/docs/errors#cancelled'), claims=[ReviewClaim(index=0, claim='The EU AI Act took effect in March 2024.', positions=None, result=ReviewResult(verdict='False', confidence='high', source='assessment', is_issue=True), assessment=ReviewAssessment(status='completed', verdict='False', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=['verdict'], disposition='planned'), verification=ReviewVerification(status='failed', content_status='available', verification_id=None, task_id='7cff18da2d979d6370e54a160be121ad', claim=None, language=None, visibility=None, depth=None, domain=None, entities=[], verdict=None, confidence=None, lenz_score=None, key_finding=None, executive_summary=None, suggested_rewrite=None, warnings=[], created_at=None, modified_at=None, verification_url=None, url=None, failure=FailureBlock(failure_reason='cancelled', failure_class='cancelled', retryable=False, hint=None, docs_url='https://lenz.io/docs/errors#cancelled')), suggested_edits=None), ReviewClaim(index=1, claim='Paris is the capital of France.', positions=None, result=ReviewResult(verdict='True', confidence='high', source='assessment', is_issue=False), assessment=ReviewAssessment(status='completed', verdict='True', confidence='high', rationale=None, dissent=None, verification_url=None, error_code=None, identified_claims=[], hint=None, suggested_rewrite=None, failure=None), escalation=Escalation(matched_rules=[], disposition='not_selected'), verification=None, suggested_edits=None)], citations=[])" +} diff --git a/tests/fixtures/parity/expected/verify__status_cancelled_durable.json b/tests/fixtures/parity/expected/verify__status_cancelled_durable.json new file mode 100644 index 0000000..820dfc6 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_cancelled_durable.json @@ -0,0 +1,61 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#cancelled", + "error": "Cancelled.", + "failure_class": "cancelled", + "failure_detail": "", + "failure_reason": "cancelled", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": false, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "error": "Cancelled.", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Cancelled.\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Cancelled.\",\n \"failure_reason\": \"cancelled\",\n \"failure_detail\": \"\",\n \"failure_class\": \"cancelled\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#cancelled\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Cancelled.', failure_reason='cancelled', failure_detail='', failure_class='cancelled', retryable=False, docs_url='https://lenz.io/docs/errors#cancelled')", + "wait": { + "cause": "Cancelled.", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: Cancelled.\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: Cancelled.", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: Cancelled.", + "status": 0 + } + }, + "request_id": "", + "retryable": false, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/verify__status_cancelled_live.json b/tests/fixtures/parity/expected/verify__status_cancelled_live.json new file mode 100644 index 0000000..b957ea0 --- /dev/null +++ b/tests/fixtures/parity/expected/verify__status_cancelled_live.json @@ -0,0 +1,61 @@ +{ + "dump": { + "candidates": [], + "claims": [], + "docs_url": "https://lenz.io/docs/errors#cancelled", + "error": "Pipeline stopped at: cancelled", + "failure_class": "cancelled", + "failure_detail": "", + "failure_reason": "cancelled", + "hint": "", + "progress": { + "elapsed_seconds": null, + "index": null, + "poll_after_seconds": null, + "step": "", + "total": null + }, + "reason": "", + "result": null, + "retryable": false, + "similar_claims": [], + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "dump_unset": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "error": "Pipeline stopped at: cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad" + }, + "render": "failed — Pipeline stopped at: cancelled\n", + "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Pipeline stopped at: cancelled\",\n \"failure_reason\": \"cancelled\",\n \"failure_detail\": \"\",\n \"failure_class\": \"cancelled\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#cancelled\"\n}\n", + "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Pipeline stopped at: cancelled', failure_reason='cancelled', failure_detail='', failure_class='cancelled', retryable=False, docs_url='https://lenz.io/docs/errors#cancelled')", + "wait": { + "cause": "Pipeline stopped at: cancelled", + "code": "", + "doc_url": "https://lenz.io/docs/errors", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "friendly_text": "Pipeline failed: Pipeline stopped at: cancelled\n Fix: Retry with a different claim, or check status.error for the diagnostic.", + "hint": "", + "message": "Pipeline failed: Pipeline stopped at: cancelled", + "payload_json": { + "error": { + "code": "pipeline_failed", + "fix": "Retry with a different claim, or check status.error for the diagnostic.", + "message": "Pipeline failed: Pipeline stopped at: cancelled", + "status": 0 + } + }, + "request_id": "", + "retryable": false, + "status_code": 0, + "task_id": "t", + "type": "LenzPipelineError" + } +} diff --git a/tests/fixtures/parity/expected/webhook__citecheck_cancelled.json b/tests/fixtures/parity/expected/webhook__citecheck_cancelled.json new file mode 100644 index 0000000..e736524 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__citecheck_cancelled.json @@ -0,0 +1,154 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "citecheck": { + "citation_failures": [ + { + "citation_index": 0, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "reference": "https://example.gov/report-2024" + }, + { + "citation_index": 1, + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "reference": "https://example.org/statement" + } + ], + "citation_issues": [], + "citations": [ + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "hint": "This source could not be checked this time. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.gov/report-2024", + "doi": null, + "index": 0, + "position": null, + "quotes": [], + "reference": "https://example.gov/report-2024", + "result": null, + "statement": "Unemployment fell to 4.1% in 2024." + }, + { + "check": { + "doi_registered": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": "This source could not be checked this time. Try again later.", + "retryable": false + }, + "hint": "This source could not be checked this time. Try again later.", + "metadata": null, + "metadata_differences": [], + "missing_quote": null, + "page_language": null, + "page_published_date": null, + "page_read": null, + "page_title": null, + "quote": null, + "rationale": null, + "registered": null, + "snippet": null, + "source_url": null, + "source_version": null, + "status": "failed", + "support": null, + "unchecked_reason": null + }, + "cited_url": "https://example.org/statement", + "doi": null, + "index": 1, + "position": null, + "quotes": [], + "reference": "https://example.org/statement", + "result": null, + "statement": "The agency said so in a statement." + } + ], + "citecheck_id": "8eb49302", + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 0 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "language": "en", + "more_citations": [], + "outcome": "incomplete", + "policy": { + "max_citations": 2 + }, + "poll_after_seconds": null, + "status": "failed", + "summary": { + "citation_checks": { + "checked": 0, + "failed": 2, + "unchecked": 0 + }, + "citation_issues": 0, + "citation_limit": 2, + "citation_limit_reached": false, + "citations_found": 2, + "citations_selected": 2 + } + }, + "citecheck_id": "8eb49302", + "delivered_at": "2026-10-01T11:07:00.123456+00:00", + "event": "citecheck.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "CitecheckEvent", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__review_cancelled.json b/tests/fixtures/parity/expected/webhook__review_cancelled.json new file mode 100644 index 0000000..62cb25f --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__review_cancelled.json @@ -0,0 +1,173 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T11:07:00.123456+00:00", + "event": "review.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review": { + "citation_failures": [], + "citation_issues": [], + "citations": [], + "claims": [ + { + "assessment": { + "confidence": "high", + "dissent": null, + "error_code": null, + "failure": null, + "hint": null, + "identified_claims": [], + "rationale": null, + "status": "completed", + "suggested_rewrite": null, + "verdict": "False", + "verification_url": null + }, + "claim": "The Eiffel Tower is in Berlin.", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "index": 0, + "positions": null, + "result": { + "confidence": "high", + "is_issue": true, + "source": "assessment", + "verdict": "False" + }, + "suggested_edits": null, + "verification": { + "claim": null, + "confidence": null, + "content_status": "available", + "created_at": null, + "depth": null, + "domain": null, + "entities": [], + "executive_summary": null, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "key_finding": null, + "language": null, + "lenz_score": null, + "modified_at": null, + "status": "failed", + "suggested_rewrite": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "url": null, + "verdict": null, + "verification_id": null, + "verification_url": null, + "visibility": null, + "warnings": [] + } + } + ], + "completed_at": "2026-10-01T10:07:00.123456Z", + "created_at": "2026-10-01T09:07:00.123456Z", + "credits": { + "charged": 1 + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "failures": [], + "issues": [ + { + "claim": "The Eiffel Tower is in Berlin.", + "claim_index": 0, + "confidence": "high", + "escalation": { + "disposition": "planned", + "matched_rules": [ + "verdict" + ] + }, + "failure": { + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_class": "cancelled", + "failure_reason": "cancelled", + "hint": null, + "retryable": false + }, + "key_finding": null, + "rationale": null, + "source": "assessment", + "suggested_edits": null, + "suggested_rewrite": null, + "url": null, + "verdict": "False", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "verified_claim": null + } + ], + "language": "en", + "more_citations": [], + "more_claim_locations": [], + "more_claims": [], + "outcome": "incomplete", + "policy": { + "confidence": [ + "low" + ], + "depth": "standard", + "max_assessments": 20, + "max_citations": 0, + "max_verifications": 5, + "suggest_edits": false, + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ] + }, + "poll_after_seconds": null, + "review_id": "d6b2bd72", + "status": "failed", + "summary": { + "assessments": { + "completed": 1, + "failed": 0 + }, + "citation_checks": null, + "citation_issues": 0, + "citation_limit": null, + "citation_limit_reached": null, + "citations_found": null, + "citations_selected": null, + "citations_skipped": null, + "claim_limit": 20, + "claim_limit_reached": false, + "claims_selected": 1, + "input_truncated": false, + "issues": 1, + "verifications": { + "completed": 0, + "failed": 1, + "planned": 1 + } + }, + "view": "full" + }, + "review_id": "d6b2bd72", + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "ReviewEvent", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/expected/webhook__verification_cancelled.json b/tests/fixtures/parity/expected/webhook__verification_cancelled.json new file mode 100644 index 0000000..aa51cf6 --- /dev/null +++ b/tests/fixtures/parity/expected/webhook__verification_cancelled.json @@ -0,0 +1,15 @@ +{ + "event": { + "attempt": 1, + "batch_id": null, + "delivered_at": "2026-10-01T11:07:00.123456+00:00", + "error": "cancelled", + "event": "verification.failed", + "failure_class": "cancelled", + "retryable": false, + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "type": "VerificationFailed", + "verification_id": null + } +} diff --git a/tests/fixtures/parity/legacy/citecheck__get_cancelled.json b/tests/fixtures/parity/legacy/citecheck__get_cancelled.json new file mode 100644 index 0000000..aa6e757 --- /dev/null +++ b/tests/fixtures/parity/legacy/citecheck__get_cancelled.json @@ -0,0 +1,148 @@ +{ + "status": 200, + "body": { + "citecheck_id": "8eb49302", + "status": "failed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 2 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "hint": "This source could not be checked this time. Try again later.", + "docs_url": "https://lenz.io/docs/errors#cancelled", + "retryable": false, + "failure_class": "cancelled", + "failure_reason": "cancelled" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "hint": "This source could not be checked this time. Try again later.", + "docs_url": "https://lenz.io/docs/errors#cancelled", + "retryable": false, + "failure_class": "cancelled", + "failure_reason": "cancelled" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "hint": "This source could not be checked this time. Try again later.", + "docs_url": "https://lenz.io/docs/errors#cancelled", + "retryable": false, + "failure_class": "cancelled", + "failure_reason": "cancelled" + } + }, + { + "citation_index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "hint": "This source could not be checked this time. Try again later.", + "docs_url": "https://lenz.io/docs/errors#cancelled", + "retryable": false, + "failure_class": "cancelled", + "failure_reason": "cancelled" + } + } + ], + "failure": { + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/review__get_cancelled.json b/tests/fixtures/parity/legacy/review__get_cancelled.json new file mode 100644 index 0000000..084ce4d --- /dev/null +++ b/tests/fixtures/parity/legacy/review__get_cancelled.json @@ -0,0 +1,197 @@ +{ + "status": 200, + "body": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 0, + "failed": 1 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "modified_at": null, + "verification_url": null, + "url": null, + "failure": { + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "status": "failed", + "content_status": "available" + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": { + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + }, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_cancelled_durable.json b/tests/fixtures/parity/legacy/verify__status_cancelled_durable.json new file mode 100644 index 0000000..d175b8e --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_cancelled_durable.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Cancelled.", + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/verify__status_cancelled_live.json b/tests/fixtures/parity/legacy/verify__status_cancelled_live.json new file mode 100644 index 0000000..2d5625b --- /dev/null +++ b/tests/fixtures/parity/legacy/verify__status_cancelled_live.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "body": { + "status": "failed", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "error": "Pipeline stopped at: cancelled", + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "Vary": "origin" + } +} diff --git a/tests/fixtures/parity/legacy/webhook__citecheck_cancelled.json b/tests/fixtures/parity/legacy/webhook__citecheck_cancelled.json new file mode 100644 index 0000000..e1ca5d9 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__citecheck_cancelled.json @@ -0,0 +1,153 @@ +{ + "status": 200, + "body": { + "event": "citecheck.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "citecheck_id": "8eb49302", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "failed", + "citecheck": { + "citecheck_id": "8eb49302", + "status": "failed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_reached": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 2 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "hint": "This source could not be checked this time. Try again later.", + "docs_url": "https://lenz.io/docs/errors#cancelled", + "retryable": false, + "failure_class": "cancelled", + "failure_reason": "cancelled" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "hint": "This source could not be checked this time. Try again later.", + "docs_url": "https://lenz.io/docs/errors#cancelled", + "retryable": false, + "failure_class": "cancelled", + "failure_reason": "cancelled" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "hint": "This source could not be checked this time. Try again later.", + "docs_url": "https://lenz.io/docs/errors#cancelled", + "retryable": false, + "failure_class": "cancelled", + "failure_reason": "cancelled" + } + }, + { + "citation_index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "hint": "This source could not be checked this time. Try again later.", + "docs_url": "https://lenz.io/docs/errors#cancelled", + "retryable": false, + "failure_class": "cancelled", + "failure_reason": "cancelled" + } + } + ], + "failure": { + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/legacy/webhook__review_cancelled.json b/tests/fixtures/parity/legacy/webhook__review_cancelled.json new file mode 100644 index 0000000..0d8dc08 --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__review_cancelled.json @@ -0,0 +1,172 @@ +{ + "status": 200, + "body": { + "event": "review.failed", + "event_id": "evt_09301870aba4e8f6ae33ff60", + "review_id": "d6b2bd72", + "task_id": "7cff18da2d979d6370e54a160be121ad", + "status": "failed", + "review": { + "review_id": "d6b2bd72", + "view": "full", + "status": "failed", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_reached": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 0, + "failed": 1 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_reached": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The Eiffel Tower is in Berlin.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "rationale": null, + "dissent": null, + "verification_url": null, + "error_code": null, + "identified_claims": [], + "hint": null, + "suggested_rewrite": null, + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "modified_at": null, + "verification_url": null, + "url": null, + "failure": { + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "status": "failed", + "content_status": "available" + }, + "suggested_edits": null + } + ], + "citations": [], + "failure": { + "failure_reason": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "hint": null, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] + }, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/fixtures/parity/legacy/webhook__verification_cancelled.json b/tests/fixtures/parity/legacy/webhook__verification_cancelled.json new file mode 100644 index 0000000..41c557d --- /dev/null +++ b/tests/fixtures/parity/legacy/webhook__verification_cancelled.json @@ -0,0 +1,19 @@ +{ + "status": 200, + "body": { + "event": "verification.failed", + "verification_id": null, + "task_id": "7cff18da2d979d6370e54a160be121ad", + "batch_id": null, + "status": "failed", + "result": null, + "needs_input": null, + "coverage": null, + "error": "cancelled", + "failure_class": "cancelled", + "retryable": false, + "attempt": 1, + "delivered_at": "2026-10-01T11:07:00.123456+00:00" + }, + "headers": {} +} diff --git a/tests/test_cancelled.py b/tests/test_cancelled.py new file mode 100644 index 0000000..cc858e3 --- /dev/null +++ b/tests/test_cancelled.py @@ -0,0 +1,383 @@ +"""A task cancelled elsewhere (the website's Stop button, another process) is +a terminal answer. + +In API version 2026-10-11 ``cancelled`` is a status of its own. In 2026-05-13 +the same thing read ``failed`` with ``failure_class: "cancelled"``, and 2.x +raised the failed error for it. A 3.x wait must end the same way, at once, +instead of polling to its timeout. The recorded bodies are the API's own +(``tests/fixtures/parity/``).""" + +from __future__ import annotations + +import json +from typing import Any + +import httpx +import pytest +import respx +from parity_observe import load + +from lenz_io import ( + CitecheckEvent, + CitecheckFailed, + Lenz, + LenzPipelineError, + LenzTimeoutError, + ReviewEvent, + ReviewFailed, + TaskStatus, + VerificationCancelled, + WebhookEvent, + parse_webhook, +) +from lenz_io.models import Citecheck, ReviewFull + +BASE = "https://lenz.io/api/v1" +_RUNNING = {"status": "processing", "task_id": "t", "progress": {"step": "research"}} +_DONE = {"status": "completed", "task_id": "u", "result": {"verification_id": "v1", "claim": "A."}} + + +def _body(name: str, shape: str = "canonical") -> dict[str, Any]: + return load(shape, name)["body"] + + +@pytest.fixture() +def slept(monkeypatch: pytest.MonkeyPatch) -> list[float]: + calls: list[float] = [] + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: calls.append(s)) + return calls + + +# ── /verify/status ── + + +@pytest.mark.parametrize("name", ["verify__status_cancelled_live.json", "verify__status_cancelled_durable.json"]) +class TestAVerificationCancelledElsewhere: + def test_get_status_returns_it_and_does_not_raise(self, client: Lenz, name: str) -> None: + body = _body(name) + with respx.mock(base_url=BASE) as r: + r.get(f"/verify/status/{body['task_id']}").respond(200, json=body) + status = client.get_status(body["task_id"]) + assert isinstance(status, TaskStatus) + assert status.status == "cancelled" + assert status.task_id == body["task_id"] + assert status.result is None and status.failure is None + + def test_wait_raises_the_failed_error_at_once(self, client: Lenz, slept: list[float], name: str) -> None: + body = _body(name) + with respx.mock(base_url=BASE) as r: + poll = r.get(f"/verify/status/{body['task_id']}") + poll.side_effect = [ + httpx.Response(200, json=_RUNNING | {"task_id": body["task_id"]}), + httpx.Response(200, json=body), + ] + with pytest.raises(LenzPipelineError) as ei: + client.wait(body["task_id"], timeout=300) + err = ei.value + assert not isinstance(err, LenzTimeoutError) + assert poll.call_count == 2 + assert (err.failure_class, err.retryable, err.failure_reason) == ("cancelled", False, "cancelled") + assert err.task_id == body["task_id"] + assert "cancelled" in err.message.lower() + + def test_verify_and_wait_raises_it(self, client: Lenz, slept: list[float], name: str) -> None: + body = _body(name) + with respx.mock(base_url=BASE) as r: + r.post("/verify").respond(200, json={"task_id": body["task_id"]}) + poll = r.get(f"/verify/status/{body['task_id']}").respond(200, json=body) + with pytest.raises(LenzPipelineError) as ei: + client.verify_and_wait("A.", timeout=300) + assert poll.call_count == 1 + assert ei.value.failure_class == "cancelled" and ei.value.retryable is False + + def test_it_is_the_error_2x_raised_for_the_original_shape( + self, client: Lenz, slept: list[float], name: str + ) -> None: + new, old = _body(name), _body(name, "legacy") + raised = [] + for body in (new, old): + with respx.mock(base_url=BASE) as r: + r.get(f"/verify/status/{body['task_id']}").respond(200, json=body) + with pytest.raises(LenzPipelineError) as ei: + client.wait(body["task_id"], timeout=300) + raised.append(ei.value) + a, b = raised + for attr in ("failure_class", "retryable", "failure_reason", "task_id", "doc_url", "hint"): + assert getattr(a, attr) == getattr(b, attr), attr + + +def test_a_batch_item_cancelled_elsewhere_is_a_failed_item(client: Lenz, slept: list[float]) -> None: + gone = _body("verify__status_cancelled_live.json") + with respx.mock(base_url=BASE) as r: + r.post("/verify/batch").respond( + 200, + json={ + "batch_id": "b", + "items": [{"task_id": gone["task_id"], "claim": "A."}, {"task_id": "u", "claim": "B."}], + }, + ) + cancelled = r.get(f"/verify/status/{gone['task_id']}").respond(200, json=gone) + done = r.get("/verify/status/u") + done.side_effect = [httpx.Response(200, json=_RUNNING | {"task_id": "u"}), httpx.Response(200, json=_DONE)] + results = client.verify_batch_and_wait(claims=[{"claim": "A."}, {"claim": "B."}], timeout=300) + assert [x.status for x in results] == ["failed", "completed"] + assert results[0].verification is None + assert results[0].status_detail is not None and results[0].status_detail.status == "cancelled" + assert cancelled.call_count == 1, "a cancelled item is not polled again" + assert done.call_count == 2 + + +def test_the_batch_waits_for_nothing_but_the_running_ones(client: Lenz, slept: list[float]) -> None: + """A batch whose only item is cancelled returns without sleeping to a timeout.""" + gone = _body("verify__status_cancelled_durable.json") + with respx.mock(base_url=BASE) as r: + r.post("/verify/batch").respond( + 200, json={"batch_id": "b", "items": [{"task_id": gone["task_id"], "claim": "A."}]} + ) + r.get(f"/verify/status/{gone['task_id']}").respond(200, json=gone) + results = client.verify_batch_and_wait(claims=[{"claim": "A."}], timeout=300) + assert [x.status for x in results] == ["failed"] + assert slept == [] + + +# ── /review ── + + +class TestAReviewCancelledElsewhere: + def test_get_review_returns_it_and_does_not_raise(self, client: Lenz) -> None: + body = _body("review__get_cancelled.json") + with respx.mock(base_url=BASE) as r: + r.get(f"/reviews/{body['review_id']}").respond(200, json=body) + review = client.get_review(body["review_id"], view="full") + assert isinstance(review, ReviewFull) + assert review.status == "cancelled" and review.outcome == "incomplete" + assert review.failure is None + + def test_review_and_wait_raises_the_failed_error_at_once(self, client: Lenz, slept: list[float]) -> None: + body = _body("review__get_cancelled.json") + rid = body["review_id"] + running = {**body, "status": "verifying", "outcome": None, "completed_at": None, "poll_after_seconds": 5} + with respx.mock(base_url=BASE) as r: + r.post("/review").respond(202, json={"review_id": rid, "status": "queued"}) + poll = r.get(f"/reviews/{rid}") + poll.side_effect = [httpx.Response(200, json=running), httpx.Response(200, json=body)] + with pytest.raises(ReviewFailed) as ei: + client.review_and_wait("draft", timeout=300) + err = ei.value + assert poll.call_count == 2 + assert (err.failure_class, err.retryable, err.failure_reason, err.error_code) == ( + "cancelled", + False, + "cancelled", + "cancelled", + ) + assert err.review_id == rid and err.review is not None and err.review.status == "cancelled" + assert "cancelled" in err.message.lower() + + def test_it_is_the_error_2x_raised_for_the_original_shape(self, client: Lenz, slept: list[float]) -> None: + raised = [] + for shape in ("canonical", "legacy"): + body = _body("review__get_cancelled.json", shape) + with respx.mock(base_url=BASE) as r: + r.get(f"/reviews/{body['review_id']}").respond(200, json=body) + with pytest.raises(ReviewFailed) as ei: + client._wait_review(body["review_id"], timeout=300) + raised.append(ei.value) + a, b = raised + for attr in ( + "failure_class", + "retryable", + "failure_reason", + "error_code", + "doc_url", + "hint", + "message", + "cause", + "fix", + ): + assert getattr(a, attr) == getattr(b, attr), attr + + +# ── /citecheck ── + + +class TestACitecheckCancelledElsewhere: + def test_get_citecheck_returns_it_and_does_not_raise(self, client: Lenz) -> None: + body = _body("citecheck__get_cancelled.json") + with respx.mock(base_url=BASE) as r: + r.get(f"/citechecks/{body['citecheck_id']}").respond(200, json=body) + check = client.get_citecheck(body["citecheck_id"]) + assert isinstance(check, Citecheck) + assert check.status == "cancelled" and check.failure is None + + def test_citecheck_and_wait_raises_the_failed_error_at_once(self, client: Lenz, slept: list[float]) -> None: + body = _body("citecheck__get_cancelled.json") + cid = body["citecheck_id"] + running = {**body, "status": "checking", "outcome": None, "completed_at": None, "poll_after_seconds": 5} + with respx.mock(base_url=BASE) as r: + r.post("/citecheck").respond(202, json={"citecheck_id": cid, "status": "queued"}) + poll = r.get(f"/citechecks/{cid}") + poll.side_effect = [httpx.Response(200, json=running), httpx.Response(200, json=body)] + with pytest.raises(CitecheckFailed) as ei: + client.citecheck_and_wait(text="See https://example.gov/report-2024.", timeout=300) + err = ei.value + assert poll.call_count == 2 + assert (err.failure_class, err.retryable, err.failure_reason, err.error_code) == ( + "cancelled", + False, + "cancelled", + "cancelled", + ) + assert err.citecheck_id == cid and err.citecheck is not None and err.citecheck.status == "cancelled" + assert "cancelled" in err.message.lower() + + def test_it_is_the_error_2x_raised_for_the_original_shape(self, client: Lenz, slept: list[float]) -> None: + raised = [] + for shape in ("canonical", "legacy"): + body = _body("citecheck__get_cancelled.json", shape) + with respx.mock(base_url=BASE) as r: + r.get(f"/citechecks/{body['citecheck_id']}").respond(200, json=body) + with pytest.raises(CitecheckFailed) as ei: + client._wait_citecheck(body["citecheck_id"], timeout=300) + raised.append(ei.value) + a, b = raised + for attr in ( + "failure_class", + "retryable", + "failure_reason", + "error_code", + "doc_url", + "hint", + "message", + "cause", + "fix", + ): + assert getattr(a, attr) == getattr(b, attr), attr + + +# ── webhooks ── + + +class TestCancelledWebhooks: + def test_verification_cancelled(self) -> None: + raw = _body("webhook__verification_cancelled.json") + event = parse_webhook(json.dumps(raw)) + assert isinstance(event, VerificationCancelled) + assert (event.event, event.status, event.task_id) == ("verification.cancelled", "cancelled", raw["task_id"]) + assert event.event_id == raw["event_id"] and event.attempt == 1 + assert event.delivered_at == raw["delivered_at"] + assert isinstance(event.verification, TaskStatus) + assert event.verification.status == "cancelled" and event.verification.task_id == raw["task_id"] + + def test_verification_cancelled_is_not_a_failed_event(self) -> None: + from lenz_io import VerificationFailed + + assert not issubclass(VerificationCancelled, VerificationFailed) + assert issubclass(VerificationCancelled, WebhookEvent) + + def test_a_cancelled_envelope_on_another_event_is_not_read_as_that_kind(self) -> None: + raw = _body("webhook__verification_cancelled.json") | {"event": "verification.failed"} + event = parse_webhook(raw) + assert event.verification is None # a failed event carrying a cancelled run + + def test_review_cancelled(self) -> None: + raw = _body("webhook__review_cancelled.json") + event = parse_webhook(raw) + assert isinstance(event, ReviewEvent) + assert (event.event, event.status, event.review_id, event.event_id) == ( + "review.cancelled", + "cancelled", + raw["review_id"], + raw["event_id"], + ) + assert event.task_id == raw["review_id"] + assert event.review is not None and event.review.status == "cancelled" + + def test_citecheck_cancelled(self) -> None: + raw = _body("webhook__citecheck_cancelled.json") + event = parse_webhook(raw) + assert isinstance(event, CitecheckEvent) + assert (event.event, event.status, event.citecheck_id, event.event_id) == ( + "citecheck.cancelled", + "cancelled", + raw["citecheck_id"], + raw["event_id"], + ) + assert event.task_id == raw["citecheck_id"] + assert event.citecheck is not None and event.citecheck.status == "cancelled" + + @pytest.mark.parametrize( + ("name", "cls"), + [ + ("webhook__verification_cancelled.json", "VerificationFailed"), + ("webhook__review_cancelled.json", "ReviewEvent"), + ("webhook__citecheck_cancelled.json", "CitecheckEvent"), + ], + ) + def test_a_cancellation_of_an_original_shape_submission_keeps_arriving_as_failed(self, name: str, cls: str) -> None: + raw = _body(name, "legacy") + event = parse_webhook(raw) + assert type(event).__name__ == cls + assert event.event.endswith(".failed") and event.status == "failed" + if cls == "VerificationFailed": + assert (event.error, event.failure_class, event.retryable) == ("cancelled", "cancelled", False) + + def test_an_unknown_event_still_parses_generically(self) -> None: + event = parse_webhook({"event": "verification.paused", "task_id": "t", "status": "paused"}) + assert type(event) is WebhookEvent + assert event.event == "verification.paused" + + +# ── the command line ── + + +class TestTheCli: + def _run(self, monkeypatch: pytest.MonkeyPatch, args: list[str], routes: dict[str, dict[str, Any]]): + from typer.testing import CliRunner + + from lenz_io.cli.app import app + + monkeypatch.setenv("LENZ_API_KEY", "lenz_" + "0" * 32) + with respx.mock(base_url=BASE) as r: + for path, body in routes.items(): + r.get(path).respond(200, json=body) + return CliRunner().invoke(app, args) + + def test_verify_resume_ends_like_a_failed_run(self, monkeypatch: pytest.MonkeyPatch) -> None: + body = _body("verify__status_cancelled_durable.json") + result = self._run( + monkeypatch, ["--json", "verify", "--resume", body["task_id"]], {f"/verify/status/{body['task_id']}": body} + ) + assert result.exit_code == 1 + assert json.loads(result.stdout)["error"]["code"] == "pipeline_failed" + assert "Unexpected status" not in result.output + + def test_status_says_cancelled(self, monkeypatch: pytest.MonkeyPatch) -> None: + body = _body("verify__status_cancelled_durable.json") + result = self._run(monkeypatch, ["status", body["task_id"]], {f"/verify/status/{body['task_id']}": body}) + assert result.exit_code == 0 and "cancelled" in result.output + + def test_review_resume_says_cancelled(self, monkeypatch: pytest.MonkeyPatch) -> None: + body = _body("review__get_cancelled.json") + result = self._run( + monkeypatch, ["review", "--resume", body["review_id"]], {f"/reviews/{body['review_id']}": body} + ) + # off a terminal the CLI prints JSON, which carries the status + assert json.loads(result.stdout)["status"] == "cancelled" + assert result.exit_code == 2 # incomplete: the same exit as any review that did not finish + + def test_citecheck_resume_says_cancelled(self, monkeypatch: pytest.MonkeyPatch) -> None: + body = _body("citecheck__get_cancelled.json") + result = self._run( + monkeypatch, ["citecheck", "--resume", body["citecheck_id"]], {f"/citechecks/{body['citecheck_id']}": body} + ) + assert json.loads(result.stdout)["status"] == "cancelled" + assert result.exit_code == 2 + + +def test_the_original_shape_still_reads_as_a_failure(client: Lenz) -> None: + """A 2.x-shaped answer is not this release's concern, but the status it + carries stays what the server said.""" + body = _body("verify__status_cancelled_durable.json", "legacy") + status = TaskStatus.model_validate(body) + assert status.status == "failed" and status.failure_class == "cancelled" diff --git a/tests/test_parity.py b/tests/test_parity.py index 46c6992..e510bfb 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -57,6 +57,40 @@ def _validation_items(body: dict[str, Any]) -> Any: return detail if isinstance(detail, list) else body.get("errors", []) +#: What a cancelled task reads differently in the current version: its own +#: status and event (the original said ``failed``), no failure block (the +#: original sent one with ``failure_class`` ``cancelled``; a wait and the +#: failed error rebuild it, which the ``wait`` paths below still hold to the +#: original), and the CLI's own words for it. +_CANCELLED_SENTENCE_PATHS = ( + "dump.error", + "wait.cause", + "wait.message", + "wait.friendly_text", + "wait.payload_json.error.message", +) + + +def _cancelled_gap(name: str, path: str) -> str | None: + last = path.rsplit(".", 1)[-1] + if path in ("dump.status", "event.status", "event.event", "event.review.status", "event.citecheck.status"): + return "a cancelled task is its own status in the current version; the original said `failed`" + if ".failure." in path and last in ("docs_url", "failure_class", "failure_reason", "hint", "retryable"): + return "the current version sends no failure block for a cancelled task" + if name == "webhook__verification_cancelled.json" and path in ( + "event.type", + "event.error", + "event.failure_class", + "event.retryable", + ): + return "`verification.cancelled` is its own event (`VerificationCancelled`), with no failure fields" + if path in ("render", "render_issues"): + return "the CLI says `Cancelled.` for a cancelled task" + if name == "verify__status_cancelled_live.json" and path in _CANCELLED_SENTENCE_PATHS: + return "a running task's original sentence was `Pipeline stopped at: cancelled`; the status reads `Cancelled.`" + return None + + def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, Any]) -> str | None: """Why ``path`` may differ for ``name``, or ``None`` when it may not.""" lb, cb = legacy["body"], canonical["body"] @@ -82,6 +116,10 @@ def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, "a failure read back from storage said 'Pipeline stopped: .'; " "the sentence is rebuilt in a running check's form" ) + if cb.get("status") == "cancelled": + why = _cancelled_gap(name, path) + if why is not None: + return why if path in ("dump.hint", "wait.hint") and lb.get("hint", "") != _failure(cb).get("hint", ""): return "the newer shape carries a hint the original did not" option_claim = path.startswith("wait.payload.claims[") and path.endswith("].claim") diff --git a/tests/test_webhook_verification.py b/tests/test_webhook_verification.py index 8ecec1d..4f43cb3 100644 --- a/tests/test_webhook_verification.py +++ b/tests/test_webhook_verification.py @@ -18,7 +18,9 @@ parse_webhook, ) -_VERIFICATION_EVENTS = [n for n in names() if n.startswith("webhook__verification_")] +# A cancellation is a different event in each shape (``verification.cancelled`` +# against ``verification.failed``): ``tests/test_cancelled.py`` reads both. +_VERIFICATION_EVENTS = [n for n in names() if n.startswith("webhook__verification_") and "cancelled" not in n] def _both(name: str) -> tuple[WebhookEvent, WebhookEvent]: From 368a995933823cc540ef32cf2667b67c5531eda6 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 21:41:20 +0300 Subject: [PATCH 46/67] docs: cancelled tasks and the *.cancelled webhook events Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 14 ++++++++++++++ README.md | 19 +++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7205e36..4e7fe13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,6 +154,20 @@ Deprecated). ### Added +- **A task cancelled elsewhere ends a wait with the failed error.** In API + version `2026-10-11` a verification, review or citation check stopped + elsewhere (the website's Stop button, another process) is its own status, + `cancelled`. `wait`, `verify_and_wait`, `verify_batch_and_wait`, + `review_and_wait` and `citecheck_and_wait` end on it at once instead of + polling to their timeout, with the error 2.x raised for the original shape + (`LenzPipelineError`, `ReviewFailed`, `CitecheckFailed`; + `failure_class` `cancelled`, `retryable` `False`); a batch item is a + `failed` row. `get_status`, `get_review` and `get_citecheck` return the + cancelled status without raising. The webhook events `verification.cancelled` + (`VerificationCancelled`), `review.cancelled` (`ReviewEvent`) and + `citecheck.cancelled` (`CitecheckEvent`) are typed; they are sent only for + work submitted with `2026-10-11`, and a cancellation of older work keeps + arriving as `*.failed`. Methods to cancel come in a later release. - `LenzNotFoundError` (404), `LenzConnectionError` and `LenzRequestTimeoutError` (see Changed). - **`retryable` on every error**, set when the error is built: whether sending diff --git a/README.md b/README.md index f837f4c..a66622f 100644 --- a/README.md +++ b/README.md @@ -230,9 +230,10 @@ The body carries the same rows as a review's: `citations`, `citation_issues`, `citation_failures`, `summary` and `more_citations` (the draft's citations past `max_citations`, found but not checked). `client.citecheck(...)` returns a `citecheck_id` at once; read it with `client.get_citecheck(citecheck_id)`. -`citecheck_and_wait` raises `CitecheckFailed` when the check fails and -`CitecheckTimeout` after `timeout` seconds. The `citecheck.completed` and -`citecheck.failed` webhooks parse into a `CitecheckEvent`. A runnable version: +`citecheck_and_wait` raises `CitecheckFailed` when the check fails (or is +cancelled elsewhere: `failure_class` `cancelled`) and `CitecheckTimeout` after +`timeout` seconds. The `citecheck.completed`, `citecheck.failed` and +`citecheck.cancelled` webhooks parse into a `CitecheckEvent`. A runnable version: [`examples/core/citecheck_draft.py`](examples/core/citecheck_draft.py). From the terminal: `lenz citecheck draft.md` (or `--pairs pairs.json`, a JSON @@ -675,6 +676,13 @@ if isinstance(event, CertificateTimestamped): It carries `coverage` instead of `result` — it reports a timestamp landing, not a verdict being produced. +A task cancelled elsewhere (the website's Stop button, another process) sends +`verification.cancelled`, parsed as `VerificationCancelled` +(`event.verification.status` is `"cancelled"`), `review.cancelled` and +`citecheck.cancelled`. These are sent for work submitted with the API version +this SDK uses; a cancellation of work submitted by an older client keeps +arriving as the `*.failed` event with `failure_class` `cancelled`. + A review sends `review.completed` or `review.failed`, parsed as `ReviewEvent` with the final review on `event.review`. Deduplicate on `event.event_id`: it is the same on every retry of one delivery. The deep checks a review runs send no @@ -907,7 +915,10 @@ A failed *verification* (as opposed to a failed HTTP call) raises `failure_class` (closed set: `upstream_unavailable` | `insufficient_evidence` | `invalid_input` | `cancelled` | `internal`) and `retryable` — `True` means a transient provider-side exhaustion where resubmitting the same claim is the -right move; older servers leave it `None`. +right move; older servers leave it `None`. A verification cancelled elsewhere +(the website's Stop button, another process) raises the same error with +`failure_class == "cancelled"` and `retryable` `False`; `review_and_wait` and +`citecheck_and_wait` do the same with `ReviewFailed` and `CitecheckFailed`. `LenzQuotaExceededError` is a **sibling** of `LenzAuthError`, not a subclass — "fix your key" and "top up your account" are different actions. So if you were From 8f08ef4e632ebe923ee1cc027fe5e8159ed38ebf Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 21:49:14 +0300 Subject: [PATCH 47/67] fix: a malformed verification.cancelled has no needs_input envelope; cancelled documented as a breaking change The verification envelope of an event now builds a cancelled status only for verification.cancelled and a pause only for verification.needs_input; any other event kind has none. A cancelled status with a null failure still reads the 2.x fields. The CLI words a cancelled batch row's details as Cancelled. The parity test anchors the failure-block exemption to the top-level blocks and compares the CLI text with the cancelled line normalised out. The example receiver handles the cancelled events. The changelog lists the cancelled status and events under Breaking and amends the receiver upgrade step. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 48 +++++++++++++++-------- examples/core/fastapi_webhook.py | 12 +++++- src/lenz_io/cli/render.py | 4 +- src/lenz_io/models.py | 2 +- src/lenz_io/webhooks.py | 6 ++- tests/test_cancelled.py | 65 +++++++++++++++++++++++++++++--- tests/test_parity.py | 27 +++++++++++-- 7 files changed, 134 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7fe13..22293fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ All notable changes to this SDK are documented here. Format follows ## [Unreleased] > **Upgrading from 2.x.** Must do: (1) upgrade every service that *receives* -> your webhooks to lenz-io 2.21+ before the sending service moves to 3.0; +> your webhooks to lenz-io 2.21+ before the sending service moves to 3.0 (a +> 2.21 receiver sees the `*.cancelled` events of 3.0-submitted work as a plain +> `WebhookEvent`: branch on `event.event` there); > (2) code reading raw bodies (`exc.body`, `event.raw`) reads the current > shape; (3) re-record tests that replay recorded 2.x response bodies; (4) an > idempotent request first sent before the switch and replayed after it raises @@ -50,6 +52,29 @@ Deprecated). - An `/extract` on an input the API first read as not a claim and then found a claim in says `status == "ready"` (2.x: `"not_a_claim"`); branch on `claims`. +- **A task cancelled elsewhere (the website's Stop button, another process) + is its own status, `cancelled`**, where 2.x read `failed` with + `failure_class` `cancelled`. + - `TaskStatus.status`, `ReviewFull.status` and `Citecheck.status` can read + `"cancelled"`, and `.failure` is `None` on those bodies. A hand-written + polling loop must treat `cancelled` as terminal or it polls until its own + timeout. A `TaskStatus` still carries the 2.x fields (`error` + `"Cancelled."`, `failure_class`, `failure_reason`, `retryable`, + `docs_url`). + - The wait helpers (`wait`, `verify_and_wait`, `verify_batch_and_wait`, + `review_and_wait`, `citecheck_and_wait`) end on it at once instead of + polling to their timeout. They raise the same error class and failure + fields (`failure_class`, `failure_reason`, `retryable`, doc URL) as 2.x did + for the original shape: `LenzPipelineError`, `ReviewFailed`, + `CitecheckFailed`. For a task cancelled while running the message reads + "Cancelled." (2.x said "Pipeline stopped at: cancelled"). A batch item is + a `failed` row; `get_status`, `get_review` and `get_citecheck` return the + cancelled status without raising. + - Webhooks for work submitted with this release arrive as + `verification.cancelled` (`VerificationCancelled`), `review.cancelled` and + `citecheck.cancelled`, not `*.failed`. A receiver that branches only on + `*.failed` misses them. A 2.21 receiver reads them as a plain + `WebhookEvent`: branch on `event.event` there. - **Webhooks of both shapes are still parsed** (`parse_webhook`, `LenzWebhooks.parse` and the event models): work submitted by an older client on the same account is delivered in the 2.x shape. @@ -60,7 +85,8 @@ Deprecated). `X-Lenz-API-Version: 2026-10-11` (`lenz_io.API_VERSION`; 2.x sent `2026-05-13`). In that shape each field, status and error code has one name across every endpoint. -- **Every attribute keeps the value it had in 2.x**, computed from the +- **Every attribute keeps the value it had in 2.x** (except the `status` of a + cancelled task, see Breaking), computed from the current shape where the server now sends it under another name or not at all. Among them: - A failed `/assess` row still reads `verdict == "Error"` and @@ -154,20 +180,10 @@ Deprecated). ### Added -- **A task cancelled elsewhere ends a wait with the failed error.** In API - version `2026-10-11` a verification, review or citation check stopped - elsewhere (the website's Stop button, another process) is its own status, - `cancelled`. `wait`, `verify_and_wait`, `verify_batch_and_wait`, - `review_and_wait` and `citecheck_and_wait` end on it at once instead of - polling to their timeout, with the error 2.x raised for the original shape - (`LenzPipelineError`, `ReviewFailed`, `CitecheckFailed`; - `failure_class` `cancelled`, `retryable` `False`); a batch item is a - `failed` row. `get_status`, `get_review` and `get_citecheck` return the - cancelled status without raising. The webhook events `verification.cancelled` - (`VerificationCancelled`), `review.cancelled` (`ReviewEvent`) and - `citecheck.cancelled` (`CitecheckEvent`) are typed; they are sent only for - work submitted with `2026-10-11`, and a cancellation of older work keeps - arriving as `*.failed`. Methods to cancel come in a later release. +- **`VerificationCancelled`** (new webhook event class, `verification.cancelled`), + and `review.cancelled` / `citecheck.cancelled` typed as `ReviewEvent` / + `CitecheckEvent`. They are sent only for work submitted with `2026-10-11`; + a cancellation of older work keeps arriving as `*.failed`. See Breaking. - `LenzNotFoundError` (404), `LenzConnectionError` and `LenzRequestTimeoutError` (see Changed). - **`retryable` on every error**, set when the error is built: whether sending diff --git a/examples/core/fastapi_webhook.py b/examples/core/fastapi_webhook.py index 021b219..446c7c5 100644 --- a/examples/core/fastapi_webhook.py +++ b/examples/core/fastapi_webhook.py @@ -27,6 +27,7 @@ LenzWebhooks, LenzWebhookSignatureError, ReviewEvent, + VerificationCancelled, VerificationCompleted, VerificationFailed, VerificationNeedsInput, @@ -70,8 +71,14 @@ async def lenz_webhook(request: Request) -> dict[str, str]: elif isinstance(event, VerificationFailed): failure = event.failure logger.warning("Verification failed: %s (%s)", event.task_id, failure.code if failure else "unknown") + elif isinstance(event, VerificationCancelled): + # Stopped elsewhere (the website's Stop button, another process). + # Work submitted by an older client reports this as verification.failed + # with failure_class "cancelled" instead. + logger.info("Verification cancelled: %s", event.task_id) elif isinstance(event, ReviewEvent): - # review.completed / review.failed: the final review, as get_review returns it. + # review.completed / review.failed / review.cancelled: the final review, + # as get_review returns it (event.status says which). if event.review is not None: logger.info( "Review %s %s: outcome %s, %d issue(s)", @@ -81,7 +88,8 @@ async def lenz_webhook(request: Request) -> dict[str, str]: len(event.review.issues), ) elif isinstance(event, CitecheckEvent): - # citecheck.completed / citecheck.failed: the final check. + # citecheck.completed / citecheck.failed / citecheck.cancelled: the final + # check (event.status says which). if event.citecheck is not None: logger.info( "Citation check %s %s: outcome %s, %d issue(s)", diff --git a/src/lenz_io/cli/render.py b/src/lenz_io/cli/render.py index b288d4b..ee7c803 100644 --- a/src/lenz_io/cli/render.py +++ b/src/lenz_io/cli/render.py @@ -485,7 +485,9 @@ def render_batch_details(out: Output, picks: list[tuple[str, str]], statuses: di st = statuses.get(tid) if st is not None and st.status == "completed" and st.result is not None: _batch_verdict_block(out, st.result) - elif st is not None and st.status in ("failed", "cancelled"): + elif st is not None and st.status == "cancelled": + out.console.print("[red]Cancelled.[/red]") + elif st is not None and st.status == "failed": out.console.print(f"[red]Failed:[/red] {st.error or st.failure_detail or 'pipeline error'}") else: label = "timed out" if st is None else (st.status or "unknown") diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 8474f74..8099dfb 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -1019,7 +1019,7 @@ class TaskStatus(_Lax): @model_validator(mode="before") @classmethod def _read_newer_shape(cls, data: Any) -> Any: - if isinstance(data, dict) and data.get("status") == "cancelled" and "failure" not in data: + if isinstance(data, dict) and data.get("status") == "cancelled" and not isinstance(data.get("failure"), dict): # A task cancelled elsewhere: its own status, with no failure # block. The 2.x fields read what the original shape said of it. return _fill( diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index 516db88..5b51aee 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -169,11 +169,15 @@ def _status_envelope(event: WebhookEvent) -> TaskStatus | None: "failure_reason": error if isinstance(error, str) else "", "failure": failure, } - else: + elif event.event == "verification.cancelled": + body = {"status": "cancelled", "task_id": raw.get("task_id") or ""} + elif event.event == "verification.needs_input": needs_input = raw.get("needs_input") given = needs_input if isinstance(needs_input, dict) else {} body = {"status": "needs_input", "task_id": raw.get("task_id") or ""} body.update({k: v for k, v in given.items() if v is not None}) + else: + return None if isinstance(body.get("result"), dict): # A key the payload left out reads as ``event.result`` reads it (the # original payload's default: ``visibility`` "private", ``depth`` diff --git a/tests/test_cancelled.py b/tests/test_cancelled.py index cc858e3..b898cc3 100644 --- a/tests/test_cancelled.py +++ b/tests/test_cancelled.py @@ -280,6 +280,22 @@ def test_a_cancelled_envelope_on_another_event_is_not_read_as_that_kind(self) -> event = parse_webhook(raw) assert event.verification is None # a failed event carrying a cancelled run + @pytest.mark.parametrize("nested", [None, "absent"]) + def test_a_cancelled_event_without_a_nested_verification_is_a_cancelled_status(self, nested: Any) -> None: + raw = {k: v for k, v in _body("webhook__verification_cancelled.json").items() if k != "verification"} + if nested is None: + raw["verification"] = None + event = parse_webhook(raw) + assert isinstance(event, VerificationCancelled) + assert event.verification is not None + assert (event.verification.status, event.verification.task_id) == ("cancelled", raw["task_id"]) + + def test_an_event_that_names_no_known_kind_has_no_verification(self) -> None: + from lenz_io.webhooks import VerificationFailed + + event = VerificationFailed(event="verification.paused", task_id="t", raw={"event": "verification.paused"}) + assert event.verification is None + def test_review_cancelled(self) -> None: raw = _body("webhook__review_cancelled.json") event = parse_webhook(raw) @@ -352,11 +368,6 @@ def test_verify_resume_ends_like_a_failed_run(self, monkeypatch: pytest.MonkeyPa assert json.loads(result.stdout)["error"]["code"] == "pipeline_failed" assert "Unexpected status" not in result.output - def test_status_says_cancelled(self, monkeypatch: pytest.MonkeyPatch) -> None: - body = _body("verify__status_cancelled_durable.json") - result = self._run(monkeypatch, ["status", body["task_id"]], {f"/verify/status/{body['task_id']}": body}) - assert result.exit_code == 0 and "cancelled" in result.output - def test_review_resume_says_cancelled(self, monkeypatch: pytest.MonkeyPatch) -> None: body = _body("review__get_cancelled.json") result = self._run( @@ -375,6 +386,50 @@ def test_citecheck_resume_says_cancelled(self, monkeypatch: pytest.MonkeyPatch) assert result.exit_code == 2 +def _styled(fn: Any, *args: Any) -> str: + """What a render prints to a terminal: the escape codes keep its styling.""" + import io + + from rich.console import Console + + from lenz_io.cli.render import Output + + buf = io.StringIO() + out = Output(json_mode=False, no_color=False) + out.json_mode = False + out.console = Console(file=buf, force_terminal=True, color_system="standard", width=100) + fn(out, *args) + return buf.getvalue() + + +class TestTheCliRenders: + def test_status_is_red_cancelled(self) -> None: + from lenz_io.cli.render import render_task_status + + text = _styled(render_task_status, TaskStatus.model_validate(_body("verify__status_cancelled_durable.json"))) + assert "\x1b[31mcancelled\x1b[0m" in text + + def test_a_batch_cell_and_its_details_say_cancelled(self) -> None: + from lenz_io.cli.render import _batch_status_cell, render_batch_details + + status = TaskStatus.model_validate(_body("verify__status_cancelled_durable.json")) + cell = _batch_status_cell(status) + assert cell.plain == "cancelled" and cell.style == "red" + text = _styled(render_batch_details, [(status.task_id, "A claim.")], {status.task_id: status}) + assert "Cancelled." in text and "Failed:" not in text and "resume:" not in text + + +def test_a_cancelled_status_with_a_null_failure_still_reads_the_2x_fields() -> None: + status = TaskStatus.model_validate({"status": "cancelled", "task_id": "t", "failure": None}) + assert (status.error, status.failure_class, status.failure_reason, status.retryable) == ( + "Cancelled.", + "cancelled", + "cancelled", + False, + ) + assert status.failure is None + + def test_the_original_shape_still_reads_as_a_failure(client: Lenz) -> None: """A 2.x-shaped answer is not this release's concern, but the status it carries stays what the server said.""" diff --git a/tests/test_parity.py b/tests/test_parity.py index e510bfb..4423735 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -75,7 +75,8 @@ def _cancelled_gap(name: str, path: str) -> str | None: last = path.rsplit(".", 1)[-1] if path in ("dump.status", "event.status", "event.event", "event.review.status", "event.citecheck.status"): return "a cancelled task is its own status in the current version; the original said `failed`" - if ".failure." in path and last in ("docs_url", "failure_class", "failure_reason", "hint", "retryable"): + top_level_block = path.startswith(("dump.failure.", "event.review.failure.", "event.citecheck.failure.")) + if top_level_block and last in ("docs_url", "failure_class", "failure_reason", "hint", "retryable"): return "the current version sends no failure block for a cancelled task" if name == "webhook__verification_cancelled.json" and path in ( "event.type", @@ -84,13 +85,29 @@ def _cancelled_gap(name: str, path: str) -> str | None: "event.retryable", ): return "`verification.cancelled` is its own event (`VerificationCancelled`), with no failure fields" - if path in ("render", "render_issues"): - return "the CLI says `Cancelled.` for a cancelled task" if name == "verify__status_cancelled_live.json" and path in _CANCELLED_SENTENCE_PATHS: return "a running task's original sentence was `Pipeline stopped at: cancelled`; the status reads `Cancelled.`" return None +#: The lines the CLI prints for a cancelled task's status, in the original +#: shape and now: the rest of the text must be the same. +_CANCELLED_RENDER_LINES = { + "failed — Cancelled.": "cancelled", + "failed — Pipeline stopped at: cancelled": "cancelled", + "Failed: cancelled": "Cancelled.", +} + + +def _cancelled_render_gap(path: str, old: Any, new: Any) -> bool: + """Whether a `render` text differs only by the line the CLI words anew for + a cancelled task.""" + if path not in ("render", "render_issues") or not isinstance(old, str) or not isinstance(new, str): + return False + lines = [_CANCELLED_RENDER_LINES.get(line, line) for line in old.splitlines()] + return lines == new.splitlines() + + def _allowed(name: str, path: str, legacy: dict[str, Any], canonical: dict[str, Any]) -> str | None: """Why ``path`` may differ for ``name``, or ``None`` when it may not.""" lb, cb = legacy["body"], canonical["body"] @@ -251,7 +268,9 @@ def test_newer_shape_reads_the_same(name: str) -> None: gaps = [ (path, old, new) for path, old, new in _diff(expected, got) - if _allowed(name, path, legacy, canonical) is None and _intended(path, old, new, legacy) is None + if _allowed(name, path, legacy, canonical) is None + and _intended(path, old, new, legacy) is None + and not (canonical["body"].get("status") == "cancelled" and _cancelled_render_gap(path, old, new)) ] assert not gaps, "\n".join(f"{path}: {old!r} -> {new!r}" for path, old, new in gaps) From ae343813f88baae3f942e271e8c5c40938dcfd8f Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 22:04:12 +0300 Subject: [PATCH 48/67] feat: cancel, cancel_review and cancel_citecheck stop a run client.cancel(task_id) returns a CancelResult (task_id, cancelled, status); cancel_review and cancel_citecheck return the same models get_review and get_citecheck do. All three answer 200 whatever the state of the run, send no body and no Idempotency-Key, and are retried on a 5xx or a dropped connection. A 409 use_review_cancel (a review's deep check) raises a LenzError whose fix names cancel_review and is sent once, never waited on or resent. The smoke test cancels a depth=low run right after it starts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- src/lenz_io/__init__.py | 2 + src/lenz_io/client.py | 75 ++++ src/lenz_io/errors.py | 6 + src/lenz_io/models.py | 15 + .../cancel_citecheck_already_cancelled.json | 138 +++++++ .../contract/cancel_citecheck_cancelled.json | 138 +++++++ .../contract/cancel_citecheck_completed.json | 103 +++++ .../cancel_review_already_cancelled.json | 182 +++++++++ .../contract/cancel_review_cancelled.json | 182 +++++++++ .../contract/cancel_review_completed.json | 91 +++++ .../contract/cancel_verify_cancelled.json | 5 + .../contract/cancel_verify_completed.json | 5 + .../contract/error_cancel_citecheck_404.json | 4 + .../contract/error_cancel_review_404.json | 4 + .../contract/error_cancel_task_404.json | 4 + .../error_cancel_use_review_cancel_409.json | 4 + tests/test_cancel.py | 367 ++++++++++++++++++ tests/test_contract.py | 3 + tests/test_public_call_bodies.py | 30 ++ tests/test_smoke_staging.py | 24 +- 20 files changed, 1381 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/contract/cancel_citecheck_already_cancelled.json create mode 100644 tests/fixtures/contract/cancel_citecheck_cancelled.json create mode 100644 tests/fixtures/contract/cancel_citecheck_completed.json create mode 100644 tests/fixtures/contract/cancel_review_already_cancelled.json create mode 100644 tests/fixtures/contract/cancel_review_cancelled.json create mode 100644 tests/fixtures/contract/cancel_review_completed.json create mode 100644 tests/fixtures/contract/cancel_verify_cancelled.json create mode 100644 tests/fixtures/contract/cancel_verify_completed.json create mode 100644 tests/fixtures/contract/error_cancel_citecheck_404.json create mode 100644 tests/fixtures/contract/error_cancel_review_404.json create mode 100644 tests/fixtures/contract/error_cancel_task_404.json create mode 100644 tests/fixtures/contract/error_cancel_use_review_cancel_409.json create mode 100644 tests/test_cancel.py diff --git a/src/lenz_io/__init__.py b/src/lenz_io/__init__.py index bb66860..11bc447 100644 --- a/src/lenz_io/__init__.py +++ b/src/lenz_io/__init__.py @@ -94,6 +94,7 @@ Audit, BatchAccepted, BatchItemResult, + CancelResult, CandidateClaim, Certificate, Citecheck, @@ -172,6 +173,7 @@ "Audit", "BatchAccepted", "BatchItemResult", + "CancelResult", "CandidateClaim", "Certificate", "CertificateTimestamped", diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 53916a4..2d5cf26 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -118,6 +118,7 @@ AssessResponse, BatchAccepted, BatchItemResult, + CancelResult, Certificate, Citecheck, CitecheckStarted, @@ -1042,6 +1043,40 @@ def get_status(self, task_id: str) -> TaskStatus: """ return self._get_status(task_id) + def cancel(self, task_id: str) -> CancelResult: + """Stop a verification that has not finished (``POST /verify/{task_id}/cancel``). + + A cancelled run is not charged and saves nothing; work already under + way (a model call in flight) is not billed to you, and the run stops + at its next step. A run waiting for ``select`` is cancelled too. + + The call is safe to repeat and answers 200 whatever the state of the + run, so losing a race is not an error. Read the result: + + - ``cancelled`` is ``True`` and ``status`` is ``"cancelled"``: the run + is cancelled, by this call or an earlier one. + - ``cancelled`` is ``False``: it had already ended. ``status`` says + how: ``"completed"`` (the verification exists and was charged as + usual) or ``"failed"``. + + ``client.wait(task_id)`` and ``get_status`` then see ``cancelled``; + ``wait`` raises :class:`LenzPipelineError` with + ``failure_class == "cancelled"``. + + Raises :class:`LenzNotFoundError` (404) for an unknown task, another + account's, or a task started on the website. A task that is a + review's deep check raises :class:`LenzError` with + ``code == "use_review_cancel"`` (409): cancel the review with + :meth:`cancel_review`, which stops everything in it. A server error or + a dropped connection is retried, as for every call that is safe to + send twice. + + Since 3.0. + """ + if not task_id: + raise ValueError("cancel() needs a task_id.") + return CancelResult.model_validate(self._request("POST", f"/verify/{task_id}/cancel")) + # ── headline ergonomic ── def verify_and_wait( @@ -1378,6 +1413,26 @@ def get_review(self, review_id: str, *, view: str = "full") -> ReviewFull | Revi raise ValueError(f"view must be 'full' or 'issues' (got {view!r}).") return ReviewFull.model_validate(self._request("GET", f"/reviews/{review_id}")) + def cancel_review(self, review_id: str) -> ReviewFull: + """Stop a review (``POST /reviews/{review_id}/cancel``), including the + deep checks it started. Returns the review as it stands afterwards, the + full view: ``status`` is ``"cancelled"``. + + A review that had already ended is returned unchanged (``completed`` or + ``failed``), and cancelling again is safe. What was not delivered is + not charged: see ``credits.charged`` on the result for what the review + cost. + + Raises :class:`LenzNotFoundError` (404) for an unknown review or + another account's, and :class:`LenzGoneError` (410) once the account's + retention period has removed it. + + Since 3.0. + """ + if not review_id: + raise ValueError("cancel_review() needs a review_id.") + return ReviewFull.model_validate(self._request("POST", f"/reviews/{review_id}/cancel")) + def review_and_wait( self, text: str, @@ -1503,6 +1558,26 @@ def get_citecheck(self, citecheck_id: str) -> Citecheck: raise ValueError("get_citecheck() needs a citecheck_id.") return Citecheck.model_validate(self._request("GET", f"/citechecks/{citecheck_id}")) + def cancel_citecheck(self, citecheck_id: str) -> Citecheck: + """Stop a citation check (``POST /citechecks/{citecheck_id}/cancel``). + Returns the check as it stands afterwards: ``status`` is + ``"cancelled"``. + + You are charged only for the citations it checked before the cancel; + the rest are refunded (``credits.charged``). A check that had already + ended is returned unchanged (``completed`` or ``failed``), and + cancelling again is safe. + + Raises :class:`LenzNotFoundError` (404) for an unknown check, another + account's, or the id of a review, and :class:`LenzGoneError` (410) once + the account's retention period has removed it. + + Since 3.0. + """ + if not citecheck_id: + raise ValueError("cancel_citecheck() needs a citecheck_id.") + return Citecheck.model_validate(self._request("POST", f"/citechecks/{citecheck_id}/cancel")) + def citecheck_and_wait( self, text: str | None = None, diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 97bc638..0b19ad8 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -897,6 +897,12 @@ def _read(key: str, block_key: str) -> Any: else: err.fix = "This run will not produce a result. Resubmit with a different claim." + if status_code == 409 and code == "use_review_cancel": + # Not "not yet": the task belongs to a review, and the review is what + # to cancel. Sending the same request again can never succeed. + err.fix = "Cancel the review that started this task instead: client.cancel_review(review_id)." + err.retryable = False + if isinstance(err, LenzGoneError): err.purged_at = _opt_str(parsed.get("purged_at")) or None # Retrying cannot bring it back, so not the generic 4xx advice. diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 8099dfb..5f90d07 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -885,6 +885,21 @@ class BatchAccepted(_Lax): items: list[TaskAccepted] = Field(default_factory=list) +class CancelResult(_Lax): + """Returned by ``POST /verify/{task_id}/cancel`` (``Lenz.cancel``). + + ``cancelled`` is ``True`` when the run is cancelled, by this call or an + earlier one; ``status`` is then ``"cancelled"``. It is ``False`` when the + run had already ended: ``status`` says how (``"completed"``, the + verification exists and was charged as usual, or ``"failed"``). The call + answers 200 either way. + """ + + task_id: str = "" + cancelled: bool = False + status: str = "" + + class Progress(_Lax): """Where a running verification has got to. Advisory — never results. diff --git a/tests/fixtures/contract/cancel_citecheck_already_cancelled.json b/tests/fixtures/contract/cancel_citecheck_already_cancelled.json new file mode 100644 index 0000000..eb3eb92 --- /dev/null +++ b/tests/fixtures/contract/cancel_citecheck_already_cancelled.json @@ -0,0 +1,138 @@ +{ + "citecheck_id": "12bbbf65", + "status": "cancelled", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 2 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } + }, + { + "citation_index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } + } + ], + "failure": null, + "more_citations": [] +} diff --git a/tests/fixtures/contract/cancel_citecheck_cancelled.json b/tests/fixtures/contract/cancel_citecheck_cancelled.json new file mode 100644 index 0000000..eb3eb92 --- /dev/null +++ b/tests/fixtures/contract/cancel_citecheck_cancelled.json @@ -0,0 +1,138 @@ +{ + "citecheck_id": "12bbbf65", + "status": "cancelled", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 0, + "unchecked": 0, + "failed": 2 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 0 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": null, + "check": { + "status": "failed", + "page_read": null, + "page_title": null, + "page_published_date": null, + "page_language": null, + "source_url": null, + "source_version": null, + "support": null, + "snippet": null, + "rationale": null, + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": "This source could not be checked this time. Try again later.", + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [ + { + "citation_index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } + }, + { + "citation_index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": "This source could not be checked this time. Try again later.", + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + } + } + ], + "failure": null, + "more_citations": [] +} diff --git a/tests/fixtures/contract/cancel_citecheck_completed.json b/tests/fixtures/contract/cancel_citecheck_completed.json new file mode 100644 index 0000000..69837e2 --- /dev/null +++ b/tests/fixtures/contract/cancel_citecheck_completed.json @@ -0,0 +1,103 @@ +{ + "citecheck_id": "12bbbf65", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "poll_after_seconds": null, + "policy": { + "max_citations": 2 + }, + "summary": { + "citations_found": 2, + "citations_selected": 2, + "citation_limit": 2, + "citation_limit_exceeded": false, + "citation_checks": { + "checked": 2, + "unchecked": 0, + "failed": 0 + }, + "citation_issues": 0 + }, + "credits": { + "charged": 2 + }, + "citations": [ + { + "index": 0, + "reference": "https://example.gov/report-2024", + "cited_url": "https://example.gov/report-2024", + "doi": null, + "statement": "Unemployment fell to 4.1% in 2024.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.gov/report-2024", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.gov/report-2024", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + }, + { + "index": 1, + "reference": "https://example.org/statement", + "cited_url": "https://example.org/statement", + "doi": null, + "statement": "The agency said so in a statement.", + "quotes": [], + "position": null, + "result": { + "finding": "supported", + "source": "support", + "is_issue": false + }, + "check": { + "status": "completed", + "page_read": "full", + "page_title": "Page at https://example.org/statement", + "page_published_date": null, + "page_language": "en", + "source_url": "https://example.org/statement", + "source_version": null, + "support": "supported", + "snippet": "The source says so.", + "rationale": "The page states it.", + "quote": null, + "doi_registered": null, + "metadata": null, + "metadata_differences": [], + "registered": null, + "unchecked_reason": null, + "hint": null, + "failure": null, + "missing_quote": null + } + } + ], + "citation_issues": [], + "citation_failures": [], + "failure": null, + "more_citations": [] +} diff --git a/tests/fixtures/contract/cancel_review_already_cancelled.json b/tests/fixtures/contract/cancel_review_already_cancelled.json new file mode 100644 index 0000000..55e47f2 --- /dev/null +++ b/tests/fixtures/contract/cancel_review_already_cancelled.json @@ -0,0 +1,182 @@ +{ + "review_id": "d6b2bd72", + "view": "full", + "status": "cancelled", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 0, + "failed": 1 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": null, + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "verification_id": null, + "task_id": "9e8d7c6b5a49382716f5e4d3c2b1a098", + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "completed_at": null, + "verification_url": null, + "url": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": null, + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "status": "failed", + "content_status": "available" + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] +} diff --git a/tests/fixtures/contract/cancel_review_cancelled.json b/tests/fixtures/contract/cancel_review_cancelled.json new file mode 100644 index 0000000..55e47f2 --- /dev/null +++ b/tests/fixtures/contract/cancel_review_cancelled.json @@ -0,0 +1,182 @@ +{ + "review_id": "d6b2bd72", + "view": "full", + "status": "cancelled", + "outcome": "incomplete", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 2, + "claims_selected": 2, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 2, + "failed": 0 + }, + "verifications": { + "planned": 1, + "completed": 0, + "failed": 1 + }, + "issues": 1, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 2 + }, + "poll_after_seconds": null, + "issues": [ + { + "claim_index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "verified_claim": null, + "verdict": "False", + "confidence": "high", + "source": "assessment", + "verification_id": null, + "verification_status": "failed", + "verification_url": null, + "url": null, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "key_finding": null, + "rationale": null, + "suggested_rewrite": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": null, + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "suggested_edits": null + } + ], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "The EU AI Act took effect in March 2024.", + "positions": null, + "result": { + "verdict": "False", + "confidence": "high", + "source": "assessment", + "is_issue": true + }, + "assessment": { + "status": "completed", + "verdict": "False", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [ + "verdict" + ], + "disposition": "planned" + }, + "verification": { + "verification_id": null, + "task_id": "9e8d7c6b5a49382716f5e4d3c2b1a098", + "claim": null, + "language": null, + "visibility": null, + "depth": null, + "domain": null, + "entities": [], + "verdict": null, + "confidence": null, + "lenz_score": null, + "key_finding": null, + "executive_summary": null, + "suggested_rewrite": null, + "warnings": [], + "created_at": null, + "completed_at": null, + "verification_url": null, + "url": null, + "failure": { + "code": "cancelled", + "detail": "The check was cancelled.", + "hint": null, + "failure_class": "cancelled", + "retryable": false, + "docs_url": "https://lenz.io/docs/errors#cancelled" + }, + "status": "failed", + "content_status": "available" + }, + "suggested_edits": null + }, + { + "index": 1, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] +} diff --git a/tests/fixtures/contract/cancel_review_completed.json b/tests/fixtures/contract/cancel_review_completed.json new file mode 100644 index 0000000..278eb24 --- /dev/null +++ b/tests/fixtures/contract/cancel_review_completed.json @@ -0,0 +1,91 @@ +{ + "review_id": "d6b2bd72", + "view": "full", + "status": "completed", + "outcome": "clean", + "created_at": "2026-10-01T09:07:00.123456Z", + "completed_at": "2026-10-01T10:07:00.123456Z", + "language": "en", + "policy": { + "verdicts": [ + "False", + "Mostly False", + "Mixed" + ], + "confidence": [ + "low" + ], + "max_assessments": 20, + "max_verifications": 5, + "depth": "standard", + "max_citations": 0, + "suggest_edits": false + }, + "summary": { + "claims_found": 1, + "claims_selected": 1, + "claim_limit": 20, + "claim_limit_exceeded": false, + "input_truncated": false, + "assessments": { + "completed": 1, + "failed": 0 + }, + "verifications": { + "planned": 0, + "completed": 0, + "failed": 0 + }, + "issues": 0, + "citations_found": null, + "citations_selected": null, + "citation_limit": null, + "citation_limit_exceeded": null, + "citation_checks": null, + "citation_issues": 0, + "citations_skipped": null + }, + "credits": { + "charged": 1 + }, + "poll_after_seconds": null, + "issues": [], + "failures": [], + "citation_issues": [], + "citation_failures": [], + "claims": [ + { + "index": 0, + "claim": "Paris is the capital of France.", + "positions": null, + "result": { + "verdict": "True", + "confidence": "high", + "source": "assessment", + "is_issue": false + }, + "assessment": { + "status": "completed", + "verdict": "True", + "confidence": "high", + "verification_url": null, + "rationale": null, + "dissent": null, + "suggested_rewrite": null, + "more_claims": [], + "failure": null + }, + "escalation": { + "matched_rules": [], + "disposition": "not_selected" + }, + "verification": null, + "suggested_edits": null + } + ], + "citations": [], + "failure": null, + "more_claims": [], + "more_claim_locations": [], + "more_citations": [] +} diff --git a/tests/fixtures/contract/cancel_verify_cancelled.json b/tests/fixtures/contract/cancel_verify_cancelled.json new file mode 100644 index 0000000..b918523 --- /dev/null +++ b/tests/fixtures/contract/cancel_verify_cancelled.json @@ -0,0 +1,5 @@ +{ + "task_id": "3f2a9c1e5b7d4a608c1d2e3f4a5b6c7d", + "cancelled": true, + "status": "cancelled" +} diff --git a/tests/fixtures/contract/cancel_verify_completed.json b/tests/fixtures/contract/cancel_verify_completed.json new file mode 100644 index 0000000..bdf5d94 --- /dev/null +++ b/tests/fixtures/contract/cancel_verify_completed.json @@ -0,0 +1,5 @@ +{ + "task_id": "3f2a9c1e5b7d4a608c1d2e3f4a5b6c7d", + "cancelled": false, + "status": "completed" +} diff --git a/tests/fixtures/contract/error_cancel_citecheck_404.json b/tests/fixtures/contract/error_cancel_citecheck_404.json new file mode 100644 index 0000000..fa2174f --- /dev/null +++ b/tests/fixtures/contract/error_cancel_citecheck_404.json @@ -0,0 +1,4 @@ +{ + "detail": "Citation check not found.", + "code": "not_found" +} diff --git a/tests/fixtures/contract/error_cancel_review_404.json b/tests/fixtures/contract/error_cancel_review_404.json new file mode 100644 index 0000000..d27beb2 --- /dev/null +++ b/tests/fixtures/contract/error_cancel_review_404.json @@ -0,0 +1,4 @@ +{ + "detail": "Review not found.", + "code": "not_found" +} diff --git a/tests/fixtures/contract/error_cancel_task_404.json b/tests/fixtures/contract/error_cancel_task_404.json new file mode 100644 index 0000000..0f2d164 --- /dev/null +++ b/tests/fixtures/contract/error_cancel_task_404.json @@ -0,0 +1,4 @@ +{ + "detail": "Task not found.", + "code": "not_found" +} diff --git a/tests/fixtures/contract/error_cancel_use_review_cancel_409.json b/tests/fixtures/contract/error_cancel_use_review_cancel_409.json new file mode 100644 index 0000000..2dd1243 --- /dev/null +++ b/tests/fixtures/contract/error_cancel_use_review_cancel_409.json @@ -0,0 +1,4 @@ +{ + "detail": "This task is a review's deep check. Cancel the review to cancel it.", + "code": "use_review_cancel" +} diff --git a/tests/test_cancel.py b/tests/test_cancel.py new file mode 100644 index 0000000..0e32bc2 --- /dev/null +++ b/tests/test_cancel.py @@ -0,0 +1,367 @@ +"""Stopping a run: ``cancel``, ``cancel_review`` and ``cancel_citecheck``. + +The bodies are the API's own (``tests/fixtures/contract/cancel_*.json`` and +``error_cancel_*.json``). A cancel is safe to repeat, so it carries no +``Idempotency-Key`` and no body; it answers 200 whatever the state of the run. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import httpx +import pytest +import respx + +from lenz_io import ( + CancelResult, + Citecheck, + Lenz, + LenzApiVersionError, + LenzAuthError, + LenzError, + LenzNotFoundError, + LenzPipelineError, + ReviewFull, +) + +BASE = "https://lenz.io/api/v1" +FIXTURES = Path(__file__).parent / "fixtures" / "contract" +TASK = "3f2a9c1e5b7d4a608c1d2e3f4a5b6c7d" +REVIEW = "d6b2bd72" +CHECK = "12bbbf65" + + +def _load(name: str) -> dict[str, Any]: + return json.loads((FIXTURES / name).read_text()) + + +@pytest.fixture() +def slept(monkeypatch: pytest.MonkeyPatch) -> list[float]: + calls: list[float] = [] + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: calls.append(s)) + return calls + + +# ── cancel(task_id) ── + + +class TestCancel: + def test_a_run_it_stopped(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/verify/{TASK}/cancel").respond(200, json=_load("cancel_verify_cancelled.json")) + result = client.cancel(TASK) + assert isinstance(result, CancelResult) + assert (result.task_id, result.cancelled, result.status) == (TASK, True, "cancelled") + assert route.call_count == 1 + + def test_a_run_that_finished_first_is_not_an_error(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + r.post(f"/verify/{TASK}/cancel").respond(200, json=_load("cancel_verify_completed.json")) + result = client.cancel(TASK) + assert (result.task_id, result.cancelled, result.status) == (TASK, False, "completed") + + def test_the_request_is_a_bare_post_with_no_key(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/verify/{TASK}/cancel").respond(200, json=_load("cancel_verify_cancelled.json")) + client.cancel(TASK) + request = route.calls.last.request + assert request.method == "POST" + assert request.content == b"" + assert "Idempotency-Key" not in request.headers + assert request.headers["X-Lenz-API-Version"] == "2026-10-11" + assert request.headers["Authorization"].startswith("Bearer ") + + def test_a_field_the_server_adds_is_kept(self, client: Lenz) -> None: + body = _load("cancel_verify_cancelled.json") | {"refunded": 10} + with respx.mock(base_url=BASE) as r: + r.post(f"/verify/{TASK}/cancel").respond(200, json=body) + result = client.cancel(TASK) + assert result.model_dump()["refunded"] == 10 + + @pytest.mark.parametrize("task_id", ["", None]) + def test_an_empty_id_sends_nothing(self, client: Lenz, task_id: Any) -> None: + with respx.mock(base_url=BASE, assert_all_called=False) as r: + route = r.post(url__regex=r".*").respond(200, json={}) + with pytest.raises(ValueError, match="cancel"): + client.cancel(task_id) + assert route.call_count == 0 + + def test_a_wait_after_a_cancel_raises_the_failed_error(self, client: Lenz, slept: list[float]) -> None: + status = {"status": "cancelled", "task_id": TASK} + with respx.mock(base_url=BASE) as r: + r.post(f"/verify/{TASK}/cancel").respond(200, json=_load("cancel_verify_cancelled.json")) + r.get(f"/verify/status/{TASK}").respond(200, json=status) + assert client.cancel(TASK).cancelled is True + with pytest.raises(LenzPipelineError) as ei: + client.wait(TASK, timeout=30) + assert (ei.value.failure_class, ei.value.retryable) == ("cancelled", False) + + @pytest.mark.parametrize("name", ["error_cancel_task_404.json"]) + def test_an_unknown_other_account_or_web_task_is_a_not_found( + self, client: Lenz, slept: list[float], name: str + ) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/verify/{TASK}/cancel").respond(404, json=_load(name)) + with pytest.raises(LenzNotFoundError) as ei: + client.cancel(TASK) + # As for every 404 the SDK maps: ``code`` is empty, the body keeps the server's. + assert (ei.value.status_code, ei.value.retryable, ei.value.body["code"]) == (404, False, "not_found") + assert route.call_count == 1 and slept == [] + + def test_a_reviews_deep_check_is_a_409_that_names_the_review_door(self, client: Lenz, slept: list[float]) -> None: + body = _load("error_cancel_use_review_cancel_409.json") + with respx.mock(base_url=BASE) as r: + route = r.post(f"/verify/{TASK}/cancel").respond(409, json=body) + with pytest.raises(LenzError) as ei: + client.cancel(TASK) + err = ei.value + assert (err.status_code, err.code) == (409, "use_review_cancel") + assert err.body == body + assert err.retryable is False + assert err.idempotency_key is None + assert "cancel_review" in err.fix + # Not the in-flight 409: one request, no wait, no resend. + assert route.call_count == 1 + assert slept == [] + + def test_the_review_409_is_not_resent_even_when_it_states_a_wait(self, client: Lenz, slept: list[float]) -> None: + body = _load("error_cancel_use_review_cancel_409.json") | {"retry_after": 1} + with respx.mock(base_url=BASE) as r: + route = r.post(f"/verify/{TASK}/cancel").respond(409, json=body, headers={"Retry-After": "1"}) + with pytest.raises(LenzError) as ei: + client.cancel(TASK) + assert ei.value.code == "use_review_cancel" + assert route.call_count == 1 and slept == [] + + def test_an_in_flight_409_is_not_a_reason_to_resend_either(self, client: Lenz, slept: list[float]) -> None: + # The resend rule is for a request that carries a key; a cancel has none. + body = {"detail": "busy", "code": "idempotency_conflict"} + with respx.mock(base_url=BASE) as r: + route = r.post(f"/verify/{TASK}/cancel").respond(409, json=body) + with pytest.raises(LenzError): + client.cancel(TASK) + assert route.call_count == 1 and slept == [] + + def test_a_server_error_is_retried_like_any_safe_call(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/verify/{TASK}/cancel") + route.side_effect = [ + httpx.Response(502, json={"detail": "bad gateway"}), + httpx.Response(200, json=_load("cancel_verify_cancelled.json")), + ] + result = client.cancel(TASK) + assert result.cancelled is True + assert route.call_count == 2 + assert len(slept) == 1 + + def test_a_dropped_connection_is_retried(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/verify/{TASK}/cancel") + route.side_effect = [ + httpx.ConnectError("reset"), + httpx.Response(200, json=_load("cancel_verify_cancelled.json")), + ] + assert client.cancel(TASK).cancelled is True + assert route.call_count == 2 + + def test_a_bad_key_is_an_auth_error(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + r.post(f"/verify/{TASK}/cancel").respond(401, json={"detail": "Invalid API key.", "code": "invalid_key"}) + with pytest.raises(LenzAuthError): + client.cancel(TASK) + + def test_it_needs_a_key(self, unauth_client: Lenz) -> None: + with respx.mock(base_url=BASE, assert_all_called=False) as r: + route = r.post(url__regex=r".*").respond(200, json={}) + with pytest.raises(LenzAuthError): + unauth_client.cancel(TASK) + assert route.call_count == 0 + + def test_an_answer_in_another_version_is_refused(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + r.post(f"/verify/{TASK}/cancel").respond( + 200, + json={"task_id": TASK, "cancelled": True, "status": "failed"}, + headers={"X-Lenz-API-Version": "2026-05-13"}, + ) + with pytest.raises(LenzApiVersionError): + client.cancel(TASK) + + +# ── cancel_review(review_id) ── + + +class TestCancelReview: + def test_a_review_it_stopped_is_the_full_view(self, client: Lenz) -> None: + body = _load("cancel_review_cancelled.json") + with respx.mock(base_url=BASE) as r: + route = r.post(f"/reviews/{REVIEW}/cancel").respond(200, json=body) + review = client.cancel_review(REVIEW) + assert isinstance(review, ReviewFull) + assert (review.review_id, review.status, review.outcome) == (REVIEW, "cancelled", "incomplete") + assert review.credits.charged == 2 + assert len(review.claims) == 2 + assert route.call_count == 1 + + def test_it_is_the_model_get_review_returns(self, client: Lenz) -> None: + body = _load("cancel_review_cancelled.json") + with respx.mock(base_url=BASE) as r: + r.post(f"/reviews/{REVIEW}/cancel").respond(200, json=body) + r.get(f"/reviews/{REVIEW}").respond(200, json=body) + cancelled = client.cancel_review(REVIEW) + read = client.get_review(REVIEW) + assert type(cancelled) is type(read) + assert cancelled.model_dump() == read.model_dump() + + @pytest.mark.parametrize( + ("name", "status"), + [ + ("cancel_review_completed.json", "completed"), + ("cancel_review_already_cancelled.json", "cancelled"), + ], + ) + def test_a_review_that_ended_is_returned_as_it_stands(self, client: Lenz, name: str, status: str) -> None: + with respx.mock(base_url=BASE) as r: + r.post(f"/reviews/{REVIEW}/cancel").respond(200, json=_load(name)) + assert client.cancel_review(REVIEW).status == status + + def test_the_request_is_a_bare_post_with_no_key(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/reviews/{REVIEW}/cancel").respond(200, json=_load("cancel_review_cancelled.json")) + client.cancel_review(REVIEW) + request = route.calls.last.request + assert request.method == "POST" + assert request.content == b"" + assert "Idempotency-Key" not in request.headers + assert request.url.params.get("view") is None + + @pytest.mark.parametrize("review_id", ["", None]) + def test_an_empty_id_sends_nothing(self, client: Lenz, review_id: Any) -> None: + with respx.mock(base_url=BASE, assert_all_called=False) as r: + route = r.post(url__regex=r".*").respond(200, json={}) + with pytest.raises(ValueError, match="cancel_review"): + client.cancel_review(review_id) + assert route.call_count == 0 + + def test_an_unknown_or_other_accounts_review_is_a_not_found(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/reviews/{REVIEW}/cancel").respond(404, json=_load("error_cancel_review_404.json")) + with pytest.raises(LenzNotFoundError) as ei: + client.cancel_review(REVIEW) + assert (ei.value.status_code, ei.value.body["code"]) == (404, "not_found") + assert route.call_count == 1 and slept == [] + + def test_a_server_error_is_retried(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/reviews/{REVIEW}/cancel") + route.side_effect = [ + httpx.Response(503, json={"detail": "down"}), + httpx.Response(200, json=_load("cancel_review_cancelled.json")), + ] + assert client.cancel_review(REVIEW).status == "cancelled" + assert route.call_count == 2 + + def test_an_answer_in_another_version_is_refused(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + r.post(f"/reviews/{REVIEW}/cancel").respond( + 200, json=_load("cancel_review_cancelled.json"), headers={"X-Lenz-API-Version": "2026-05-13"} + ) + with pytest.raises(LenzApiVersionError): + client.cancel_review(REVIEW) + + def test_it_needs_a_key(self, unauth_client: Lenz) -> None: + with pytest.raises(LenzAuthError): + unauth_client.cancel_review(REVIEW) + + +# ── cancel_citecheck(citecheck_id) ── + + +class TestCancelCitecheck: + def test_a_check_it_stopped_is_the_citecheck_model(self, client: Lenz) -> None: + body = _load("cancel_citecheck_cancelled.json") + with respx.mock(base_url=BASE) as r: + route = r.post(f"/citechecks/{CHECK}/cancel").respond(200, json=body) + check = client.cancel_citecheck(CHECK) + assert isinstance(check, Citecheck) + assert (check.citecheck_id, check.status, check.outcome) == (CHECK, "cancelled", "incomplete") + assert check.credits.charged == 0 + assert route.call_count == 1 + + def test_it_is_the_model_get_citecheck_returns(self, client: Lenz) -> None: + body = _load("cancel_citecheck_cancelled.json") + with respx.mock(base_url=BASE) as r: + r.post(f"/citechecks/{CHECK}/cancel").respond(200, json=body) + r.get(f"/citechecks/{CHECK}").respond(200, json=body) + cancelled = client.cancel_citecheck(CHECK) + read = client.get_citecheck(CHECK) + assert type(cancelled) is type(read) + assert cancelled.model_dump() == read.model_dump() + + @pytest.mark.parametrize( + ("name", "status"), + [ + ("cancel_citecheck_completed.json", "completed"), + ("cancel_citecheck_already_cancelled.json", "cancelled"), + ], + ) + def test_a_check_that_ended_is_returned_as_it_stands(self, client: Lenz, name: str, status: str) -> None: + with respx.mock(base_url=BASE) as r: + r.post(f"/citechecks/{CHECK}/cancel").respond(200, json=_load(name)) + assert client.cancel_citecheck(CHECK).status == status + + def test_the_request_is_a_bare_post_with_no_key(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/citechecks/{CHECK}/cancel").respond(200, json=_load("cancel_citecheck_cancelled.json")) + client.cancel_citecheck(CHECK) + request = route.calls.last.request + assert request.method == "POST" + assert request.content == b"" + assert "Idempotency-Key" not in request.headers + + @pytest.mark.parametrize("citecheck_id", ["", None]) + def test_an_empty_id_sends_nothing(self, client: Lenz, citecheck_id: Any) -> None: + with respx.mock(base_url=BASE, assert_all_called=False) as r: + route = r.post(url__regex=r".*").respond(200, json={}) + with pytest.raises(ValueError, match="cancel_citecheck"): + client.cancel_citecheck(citecheck_id) + assert route.call_count == 0 + + @pytest.mark.parametrize("name", ["error_cancel_citecheck_404.json"]) + def test_an_unknown_check_or_a_review_id_is_a_not_found(self, client: Lenz, slept: list[float], name: str) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/citechecks/{CHECK}/cancel").respond(404, json=_load(name)) + with pytest.raises(LenzNotFoundError) as ei: + client.cancel_citecheck(CHECK) + assert (ei.value.status_code, ei.value.body["code"]) == (404, "not_found") + assert route.call_count == 1 and slept == [] + + def test_a_server_error_is_retried(self, client: Lenz, slept: list[float]) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(f"/citechecks/{CHECK}/cancel") + route.side_effect = [ + httpx.Response(500, json={"detail": "oops"}), + httpx.Response(200, json=_load("cancel_citecheck_cancelled.json")), + ] + assert client.cancel_citecheck(CHECK).status == "cancelled" + assert route.call_count == 2 + + def test_an_answer_in_another_version_is_refused(self, client: Lenz) -> None: + with respx.mock(base_url=BASE) as r: + r.post(f"/citechecks/{CHECK}/cancel").respond( + 200, json=_load("cancel_citecheck_cancelled.json"), headers={"X-Lenz-API-Version": "2026-05-13"} + ) + with pytest.raises(LenzApiVersionError): + client.cancel_citecheck(CHECK) + + def test_it_needs_a_key(self, unauth_client: Lenz) -> None: + with pytest.raises(LenzAuthError): + unauth_client.cancel_citecheck(CHECK) + + +def test_the_cancel_result_is_lax_like_the_other_models() -> None: + result = CancelResult.model_validate({}) + assert (result.task_id, result.cancelled, result.status) == ("", False, "") diff --git a/tests/test_contract.py b/tests/test_contract.py index 10a99ad..7ff4864 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -35,6 +35,7 @@ from lenz_io.models import ( AssessClaim, AssessResponse, + CancelResult, Certificate, Citecheck, CitecheckStarted, @@ -213,6 +214,8 @@ def _load(name: str) -> dict: ("citecheck_accepted.json", CitecheckStarted), ("citecheck_completed.json", Citecheck), ("citecheck_pairs_completed.json", Citecheck), + ("cancel_verify_cancelled.json", CancelResult), + ("cancel_verify_completed.json", CancelResult), ], ) def test_contract_no_unknown_fields(fixture_name, model_cls): diff --git a/tests/test_public_call_bodies.py b/tests/test_public_call_bodies.py index af4226c..c174ef1 100644 --- a/tests/test_public_call_bodies.py +++ b/tests/test_public_call_bodies.py @@ -14,6 +14,7 @@ from __future__ import annotations +import inspect import json from pathlib import Path from typing import Any @@ -388,6 +389,35 @@ def test_citecheck_and_wait( assert route.calls.last.request.headers["Idempotency-Key"] == KEY +# ── cancel / cancel_review / cancel_citecheck ────────────────────────────── + + +_ID = {"cancel": "task_id", "cancel_review": "review_id", "cancel_citecheck": "citecheck_id"} + + +class TestCancels: + """A cancel is a POST with no body and no key: it is safe to repeat, so + there is nothing for an ``Idempotency-Key`` to replay.""" + + @pytest.mark.parametrize( + ("method", "path", "fixture"), + [ + ("cancel", "/verify/t1/cancel", "cancel_verify_cancelled.json"), + ("cancel_review", "/reviews/r1/cancel", "cancel_review_cancelled.json"), + ("cancel_citecheck", "/citechecks/c1/cancel", "cancel_citecheck_cancelled.json"), + ], + ) + def test_no_body_no_key(self, client: Lenz, method: str, path: str, fixture: str) -> None: + with respx.mock(base_url=BASE) as r: + route = r.post(path).respond(200, json=_load(fixture)) + getattr(client, method)(path.split("/")[2]) + request = route.calls.last.request + assert request.content == b"" + assert "Idempotency-Key" not in request.headers + # The id is the only parameter: no option can reach the wire. + assert list(inspect.signature(getattr(Lenz, method)).parameters) == ["self", _ID[method]] + + # ── 3.0: every forwarded option is a named, keyword-only parameter ───────── diff --git a/tests/test_smoke_staging.py b/tests/test_smoke_staging.py index f067060..86149c7 100644 --- a/tests/test_smoke_staging.py +++ b/tests/test_smoke_staging.py @@ -10,6 +10,7 @@ 3. ``verify_and_wait`` — the quickstart claim at ``depth="low"``, the cheap run (~60s); a cache hit is a bonus, never assumed 4. ``ask.history`` — read-only follow-up surface (no exchange burned) + 5. ``cancel`` — a ``depth="low"`` run stopped right after it starts Plus webhook signature roundtrip + ``/me/usage`` shape. @@ -27,7 +28,7 @@ import pytest -from lenz_io import Lenz, LenzWebhooks, verify_signature +from lenz_io import Lenz, LenzPipelineError, LenzWebhooks, verify_signature pytestmark = pytest.mark.smoke @@ -57,6 +58,27 @@ def test_quickstart_claim_verifies_at_low_depth(smoke_client): assert v.verdict # any non-empty verdict string +def test_a_run_can_be_cancelled(smoke_client): + """``cancel`` answers 200 whatever the state of the run: stopped by this + call, or already ended. Neither is an error, so the test cannot be flaky. + + A cancelled run is not charged. A cache hit (an answer the API already + holds) has finished by the time we cancel, which is the other branch.""" + started = smoke_client.verify(claim="The Eiffel Tower is in Paris", depth="low") + result = smoke_client.cancel(started.task_id) + assert result.task_id == started.task_id + if result.cancelled: + assert result.status == "cancelled" + # Safe to repeat: still cancelled, and a wait ends on it at once. + assert smoke_client.cancel(started.task_id).cancelled is True + with pytest.raises(LenzPipelineError) as raised: + smoke_client.wait(started.task_id, timeout=60) + assert raised.value.failure_class == "cancelled" + else: + # It finished first; nothing was left to stop. + assert result.status in ("completed", "failed") + + def test_assess_returns_typed_claims(smoke_client): """``/assess`` is sync, ~15s. Returns one entry per identified claim.""" out = smoke_client.assess(text="Sharks don't get cancer") From e85d79529734066f2270ffdb92ba2dfe7fe36001 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 22:04:40 +0300 Subject: [PATCH 49/67] docs: stopping a run with cancel, cancel_review and cancel_citecheck Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 17 +++++++++++++++++ README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22293fc..4d80dd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -180,6 +180,23 @@ Deprecated). ### Added +- **Stopping a run: `cancel`, `cancel_review` and `cancel_citecheck`.** + `client.cancel(task_id)` stops a verification and returns a `CancelResult` + (`task_id`, `cancelled`, `status`); `client.cancel_review(review_id)` stops a + review, its deep checks and its citation checks, and returns the full + `ReviewFull` (what `get_review` returns); `client.cancel_citecheck(citecheck_id)` + returns the `Citecheck`. All three answer 200 whatever the state of the run: + `cancelled=False` on a `cancel` means the run had already ended, and + `status` says how (`completed` or `failed`); a review or check that had + ended comes back unchanged. They send no body and no `Idempotency-Key` + (cancelling twice is safe), and are retried on a 5xx or a dropped + connection like any call that is safe to repeat. An unknown id, another + account's, or (for `cancel`) a task started on the website raises + `LenzNotFoundError`. A task that is a review's deep check raises a + `LenzError` with `code == "use_review_cancel"` (409): cancel the review + instead; it is sent once, never waited on or resent. A cancelled run is not + charged; a `wait` on it raises the failed error with + `failure_class == "cancelled"`. Needs the API to serve version `2026-10-11`. - **`VerificationCancelled`** (new webhook event class, `verification.cancelled`), and `review.cancelled` / `citecheck.cancelled` typed as `ReviewEvent` / `CitecheckEvent`. They are sent only for work submitted with `2026-10-11`; diff --git a/README.md b/README.md index a66622f..fa29f33 100644 --- a/README.md +++ b/README.md @@ -949,6 +949,55 @@ if status.status == "completed": print(status.result.verdict, status.result.lenz_score) ``` +## Stopping a run + +A verification, a review or a citation check that is still running can be +stopped. Each has its own method, and each answers 200 whatever the state of +the run, so losing a race is not an error: + +```python +result = client.cancel("tsk_abc123") # a verification -> CancelResult +if result.cancelled: + print("stopped:", result.status) # "cancelled" +else: + print("already ended:", result.status) # "completed" or "failed" + +review = client.cancel_review("d6b2bd72") # the full view, like get_review +print(review.status, review.credits.charged) # "cancelled", what it cost + +check = client.cancel_citecheck("12bbbf65") # like get_citecheck +print(check.status, check.credits.charged) +``` + +- `cancel(task_id)` returns a `CancelResult`. `cancelled=True` with + `status == "cancelled"` means the run is cancelled, by this call or an + earlier one. `cancelled=False` means it had already ended: `status` is + `"completed"` (the verification exists and was charged as usual) or + `"failed"`. A run waiting on `select` is cancelled too. +- `cancel_review(review_id)` stops the review and everything in it: its quick + checks, its deep checks and its citation checks. It returns the review as it + stands, `status == "cancelled"`; a review that had already ended is returned + unchanged (`completed` or `failed`). +- `cancel_citecheck(citecheck_id)` returns the check the same way. +- A review's deep checks are cancelled **through the review**. Calling + `cancel(task_id)` with the `task_id` of one raises a `LenzError` with + `code == "use_review_cancel"` (HTTP 409); call `cancel_review` instead. The + SDK sends that request once and does not wait or resend. +- An unknown id, another account's, or (for `cancel`) a task started on the + website raises `LenzNotFoundError`. +- A cancelled verification is not charged and saves nothing. A cancelled + review or citation check is charged only for what it delivered before the + cancel (quick checks served, deep checks that finished, citations checked); + the rest is refunded or never charged. `credits.charged` on the result is + what the review or check cost. +- Cancelling twice is safe, so the calls send no `Idempotency-Key`. They are + retried on a 5xx or a dropped connection, like any call that is safe to + repeat. +- Afterwards `get_status`, `get_review` and `get_citecheck` return the + `cancelled` status, and `wait`, `review_and_wait` and `citecheck_and_wait` + raise the failed error with `failure_class == "cancelled"`. A `webhook_url` + receives `verification.cancelled`, `review.cancelled` or `citecheck.cancelled`. + ## Idempotency Every call that charges or starts work sends an auto-generated From 7cab559415b28a2d16601ce7c389914ac429871d Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 22:06:12 +0300 Subject: [PATCH 50/67] chore: refresh openapi.json (API version 2026-10-11 and the cancel endpoints) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- openapi.json | 2471 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 1493 insertions(+), 978 deletions(-) diff --git a/openapi.json b/openapi.json index 8768c4d..0d48ff3 100644 --- a/openapi.json +++ b/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Lenz \u2014 AI Fact-Checking API", "version": "1.0.0", - "description": "# Four API calls and one to run them\n\nA research-depth ladder: find claims, judge fast, prove deep, citecheck; `/review` runs them all.\n\n- `POST /extract` \u2014 enumerate every major verifiable claim in a document (up to 100, most check-worthy first, inputs to 50k chars), or in a public web page given its URL; an optional `focus` narrows the list to the claims you care about, and `locate: true` keeps only the claims traced back to the text, with where each is made. Costs no credits; fair-use cap 1,000/account/day.\n- `POST /assess` \u2014 fast 3-model frontier panel verdict, typically 10-25s. Sync, 1 credit per claim. Send one text in `claim`, or a list of up to 20 in `claims` \u2014 one row per claim, same order, all checked in parallel. A text that makes more claims than one call checks gets the most check-worthy checked and the rest back as `more_claims`, free, to send on.\n- `POST /verify` \u2014 full multi-model pipeline with citations in ~90s. Async, 10 credits \u2014 or 5 with `depth: \"low\"`, a shallower check that comes back sooner.\n- `POST /citecheck` \u2014 does each cited source say what the text attributes to it: send pairs of a statement and its URL or DOI, or a whole text. Async; 1 credit per checked citation, refunded when a citation could not be checked ([guide](https://lenz.io/docs/citations)).\n- `POST /review` \u2014 the extract \u2192 assess \u2192 verify ladder on a whole draft, returning the issues with suggested rewrites in 2-4 min. Async, 1 credit per claim assessed + 10 (5 at `depth: \"low\"`) per claim verified. With `escalate.max_citations` (0-20, default 0) it also checks whether each source the draft cites says what the draft attributes to it, at 1 credit per checked citation (one that could not be checked is refunded) ([guide](https://lenz.io/docs/citations)). `more_claims` and `more_citations` list what the review found and did not check, at no cost; send them to `/assess` and `/citecheck` to check the rest.\n- Also `POST /ask/{id}` \u2014 ask follow-up questions grounded on a verification.\n\n## Built for teams whose AI output is async or document-shaped\n\nLegal-memo generators, AI deep-research, due-diligence platforms, vertical agents producing structured deliverables, \"report-as-a-service\" companies. `/assess` opens the door to sync UX too \u2014 fast enough to gate a chat completion or a UI submit.\n\n## API versions\n\nEvery request is answered in version `2026-05-13` for now. This reference describes the next version, which is not available yet: what `2026-05-13` sends instead is marked here. A field it still sends under an older name is listed `deprecated` beside the current one (\"Sent only to callers on `2026-05-13`\"), and where its shape differs, the operation names the `...Legacy` component it receives (`x-lenz-versions` on the response). Every response carries `X-Lenz-API-Version`, naming the version it is in.\n\n## Install the SDK\n\n```bash\npip install lenz-io # Python\nnpm install lenz-io # Node / TypeScript\n```\n\nSource on GitHub: [lenz-io-python](https://github.com/lenzhq/lenz-io-python) \u00b7 [lenz-io-node](https://github.com/lenzhq/lenz-io-node).\n\n## The canonical integration: one /review call\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# Extract, assess, and verify the doubtful claims in one call (async, 2-4 min)\ndraft = \"The EU AI Act took effect in March 2024. It sorts AI systems into four risk tiers.\"\nreview = client.review_and_wait(text=draft)\nfor i in review.issues:\n print(i.verdict, i.claim, i.suggested_rewrite)\n```\n\nThe default policy: a quick verdict on every claim (up to 20), and the deep check on the rows whose quick verdict is `False`, `Mostly False` or `Mixed`, or whose confidence is `low`, up to five. Change it in `escalate`. To deep-check the rows the cap left out (`escalation.disposition == \"cap\"`), send them to `/verify/batch`. A resend with the same `Idempotency-Key` within 24 hours returns the same review; a new key is a new review.\n\n## The primitives, call by call: extract \u2192 assess \u2192 conditional verify\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# 1. Enumerate the factual claims in your model output (free)\n# identified_claims is the complete ordered list when >1 claim is\n# found (top claim included \u2014 iterate it alone); for a single claim\n# it is [] and the claim lives in .claim.\n# Pass focus=\"market size and competitors\" to narrow the list;\n# status is \"no_match\" when the document has claims but none match.\nextracted = client.extract(text=llm_output)\nclaims = extracted.identified_claims or [extracted.claim]\n\n# 2. Fast verdict on every claim in ONE call (typically 10-25s, 3-model panel):\n# a list of up to 20, one row per claim, in the order you sent them.\nquick = client.assess(claims=claims).claims\n\n# 3. Escalate the low-confidence ones to /verify in one batch (~90s, multi-model pipeline)\n# Rows we ran out of time on are free and worth resending; the rest of the\n# Error rows (no_claim, framing_failed) want a different input.\nretry = [c.claim for c in quick if c.error_code == \"timeout\"]\nquick += client.assess(claims=retry).claims if retry else []\ndoubtful = [{\"claim\": c.claim} for c in quick if c.verdict != \"Error\" and c.confidence == \"low\"]\nfor r in client.verify_batch_and_wait(claims=doubtful) if doubtful else []:\n if r.verification:\n print(r.verification.verdict, r.verification.lenz_score, r.verification.key_finding)\n```\n\n## How /verify works under the hood\n\nFrame \u2192 Collect Evidence \u2192 Debate (2 models, 2 rounds) \u2192 Adjudicate (three reviewers on models from different vendors, two more when they disagree) \u2192 Conclude. ~90 seconds wall-clock per claim.\n\n### While /verify is running\n\n`GET /verify/status/{task_id}` answers one of four shapes, keyed on `status`: `processing`, `needs_input`, `completed`, `failed`. Every one echoes `task_id`, and only the fields belonging to that shape are present \u2014 read the ones for the branch you are in, and treat an absent key as absent rather than empty.\n\nA `processing` body carries `progress`:\n\n```json\n{\"status\": \"processing\", \"task_id\": \"\u2026\",\n \"progress\": {\"step\": \"research\", \"index\": 2, \"total\": 5,\n \"elapsed_seconds\": 42, \"poll_after_seconds\": 5}}\n```\n\n`step` is a closed set \u2014 `starting`, `framing`, `research`, `debate`, `adjudication`, `conclusion` \u2014 so it is safe to switch on. `index` is the 1-based stage position (0 while `starting`) out of `total`; read `total` off the response rather than hard-coding it. `poll_after_seconds` is how long to wait before looking again, and `elapsed_seconds` is how long the run has been going \u2014 a measurement, not an estimate of what is left.\n\nNote that `index` is stage **position**, not elapsed work: the stages are uneven, so a progress bar driven by it will sit on `research` for roughly half the run.\n\n`progress` is **advisory**. It says the work is pending and which stage it is on; it is not a place to read results from. The verdict arrives once, under `result`, when `status` is `completed`.\n\n### When /verify asks for input\n\n`/verify` is not only submit-and-poll. `GET /verify/status/{task_id}` can terminate at `needs_input` \u2014 the submitted text held several claims, or was too vague to check as written \u2014 and the task waits rather than guessing which claim you meant. Several claims in one sentence count the same as several sentences.\n\nResolve it with `POST /verify/{task_id}/select`, passing the exact wording of the claim(s) you want from the ones the status offered. Selection is **by text, not index**; anything that was not offered is rejected with a 422. Each selected claim fans out into its own pipeline, so the response carries one `task_id` per claim \u2014 poll each the way you would a fresh `/verify`.\n\nClaims returned by `/extract` skip this entirely: submitted verbatim, they are never bounced back for rephrasing.\n\n### Covered verification\n\nEvery verification a paid Pro or Scale account runs is put in front of a gate. The ones that qualify carry a contractual warranty, and every result tells you whether it qualified \u2014 a `coverage` block on the completed `/verify/status/{task_id}` body, on `GET /verifications/{id}` and on the `verification.completed` webhook.\n\nIt is on by default, with no request field to set; an account on Pro or Scale can turn certificates off on the API credentials page, for checks submitted from then on (reason `account`; a verification that already carries a certificate keeps it).\n\n`status` is `covered`, `pending_timestamp` or `uncovered`; `reasons` says why not, in a closed seven-value vocabulary. **If you publish automatically, key on the `certificate.timestamped` webhook, not `verification.completed`** \u2014 cover requires the qualified timestamp to precede what you publish, and `completed` fires while the qualified timestamp is still pending.\n\nFull wire format, the reason vocabulary and the offline verifier: [lenz.io/docs/coverage](https://lenz.io/docs/coverage). What is covered and what is not: [lenz.io/warranty](https://lenz.io/warranty).\n\n### When /verify fails\n\nA `failed` status (and the `verification.failed` webhook) carries two axes. `failure_reason` says **where** the pipeline stopped (`research_empty`, `conclusion_failed`, `task_stuck`, \u2026); on the webhook the same value rides in `error`. `failure_class` says **why**, and `retryable` is derived from it:\n\n| `failure_class` | Meaning | `retryable` |\n|---|---|---|\n| `upstream_unavailable` | Model or search providers were rate-limited or down, or the task was never run \u2014 resubmit after a short wait | `true` |\n| `insufficient_evidence` | We looked; the sources are not there | `false` |\n| `invalid_input` | Not a checkable claim, an unreadable URL | `false` |\n| `cancelled` | You stopped it | `false` |\n| `internal` | Anything else \u2014 retrying the same input will not help | `false` |\n\nBranch on `retryable`, not on `failure_reason`: the same reason can mean either. `failure_class` is a **closed set** \u2014 these five values are stable identifiers. `failure_reason` is an open, informational set (`task_error`, `task_stuck`, `not_a_claim`, \u2026 will appear). A failed body also carries `docs_url`, pointing at the explanation of its class. A failed `/verify` is not charged.\n\nOne of those reasons comes from framing rather than from a pipeline step and carries `failure_class: invalid_input`: `not_a_claim` (the input is not a checkable statement, or is too vague to have a checkable reading). A vague input that has a likely reading is verified on it, and the result's `claim` is that reading. On the webhook the same value rides in `error`, as an ordinary `verification.failed` delivery; before 2026-09-03 it fired nothing at all, so a task submitted with a `webhook_url` could terminate in silence.\n\n### What may change without notice\n\nNew keys may be added to status and webhook bodies at any time \u2014 ignore keys you do not recognise. Fields documented as **advisory** \u2014 `progress` and everything inside it \u2014 may also change or disappear without a version bump. The verdict fields under `result` may not: those change only with a versioned, announced release.\n\n## Common patterns\n\n- **Runtime sync UX**: `POST /assess` on the user-facing path; escalate `confidence == \"low\"` claims to `/verify` in the background.\n- **Runtime webhook**: `POST /verify` with `webhook_url`. Lenz POSTs the typed payload when done. Best for document pipelines.\n- **Whole drafts**: `POST /review` with `webhook_url`; handle `review.completed` (a `ReviewWebhookPayload`, deduped on `event_id`) and read `review.issues[]` and `review.citation_issues[]`.\n- **CI / pre-release**: `/extract` + batch `/verify` your golden set on every deploy. The free `/extract` tier covers most of this. Claims returned by `/extract` verify cleanly \u2014 submitted verbatim to `/verify`, they are never bounced back for rephrasing.\n- **Incident triage**: paste the offending output into `/extract`, then `/verify` the wrong-looking claims. Get a citation trail to send back to the customer.\n\n## What's in the response\n\nUnified vocabulary across every claim-shaped response:\n\n- **`claim`** \u2014 the framed claim text.\n- **`verdict`** \u2014 `\"True\" | \"Mostly True\" | \"Mixed\" | \"Mostly False\" | \"False\" | \"Error\"`.\n- **`confidence`** \u2014 `\"high\" | \"medium\" | \"low\"`.\n- **`lenz_score`** \u2014 1\u201310 integer score (deep payloads).\n- **`key_finding`** \u2014 one declarative sentence stating the most important fact the analysis established. For a false claim it states the CORRECTED fact rather than restating the claim, so render it next to the verdict label, never as a standalone headline. `\"\"` on older verifications predating the field.\n- **`suggested_rewrite`** \u2014 a suggested rewrite of `claim` that the verification's findings support. It has not been verified itself: before using it, review it or run it through `/verify`. `null` for a true claim, when no correction is established, and on older verifications. On every verification, single or listed (`GET /verifications/{id}`, `/verify/status`, the `verification.completed` webhook, `GET /verifications`, `GET /library`). On `/assess` rows too when the request sets `suggest_rewrite`: written from the quick check's reasoning for a claim it found False or Mostly False with high confidence, at no extra credit; `null` otherwise, and on every row when not requested.\n- **`sources`** \u2014 evidence with title, URL and snippet: the sentence(s) quoted from the page, verbatim and in the page's own language (`/verify` only).\n- **`audit`** \u2014 adjudication reasoning, debate transcript, panel agreement (`/verify` only).\n\n## How the API is organized\n\n- **Extract** \u2014 pull verifiable claims out of text.\n- **Assess** \u2014 fast 3-model verdict.\n- **Verify** \u2014 deep multi-model pipeline: submit one claim or a batch, poll status, and resolve a `needs_input` interrupt by selecting claims.\n- **Verifications** \u2014 list, fetch, delete, and toggle visibility on stored verifications.\n- **Library** \u2014 browse the public catalog. No API key needed.\n- **Ask** \u2014 follow-up questions, 1 credit each.\n- **Account** \u2014 your credit balance and the price list.\n\n## Credits\n\nOne pool per account funds every billable call, at a fixed weight:\n\n| Endpoint | Credits |\n|---|---|\n| `POST /verify` (and `/verify/batch`, `/select`) | 10 per claim |\n| `POST /verify` with `depth: \"low\"` | 5 per claim |\n| `POST /assess` | 1 per claim \u2014 per row that carries a verdict; error rows and rows answered from the cache are free |\n| `POST /ask` | 1 |\n| `POST /extract` | 0 \u2014 free, fair-use capped |\n\n`depth: \"low\"` caps research breadth \u2014 fewer discovery queries, a hard extraction ceiling, no recovery tiers on the happy path \u2014 while every reasoning step runs unchanged. It is not a model downgrade. You are charged for the depth you REQUESTED. A `/verify/batch` may mix depths per item and is billed per item.\n\nAn answer served from the last hour's cache is free: a `/verify`, `/assess` or `/review` claim that gets back a verdict checked in the last hour costs nothing, so a tool that resends a request is not charged twice. The exception is a `/verify` that issues you a new warranty certificate, charged at the depth you requested.\n\n`GET /me/usage` reports the balance under `credits` and the weights under `costs`, with parameter-dependent prices such as `depth` nested under `cost_options`. The per-capability blocks beside them (`verify`, `assess`, `ask`) are **projections** of that one balance into each capability's own unit \u2014 how many of those calls the remaining credits would buy \u2014 not separate allowances. Spending on any one of them moves all of them.\n\n## Authentication\n\n1. [Create a Lenz account](https://lenz.io/auth).\n2. Generate an API key on the [API credentials](https://lenz.io/api-credentials) page.\n3. Send it on every request:\n ```\n Authorization: Bearer lenz_...\n ```\n\nOr skip steps 1\u20132: **[lenz.io/setup](https://lenz.io/setup)** issues a test key and generates setup instructions for your environment \u2014 MCP client, SDK or plain HTTP \u2014 that you paste into your coding agent to do the wiring.\n\n### One balance, whichever door you come through\n\nLenz also runs a remote MCP server at `https://lenz.io/mcp`, so an agent can fact-check inside a conversation. It is **not** part of this surface \u2014 there are no MCP endpoints below, and its tools are documented at [lenz.io/integrations/mcp-server](https://lenz.io/integrations/mcp-server).\n\nIt matters here for one reason: that server is a client of this API. It takes the same `lenz_...` key and forwards it to the endpoints below, so MCP traffic spends the same credits, counts against the same rate limits, and shows up in `GET /me/usage` \u2014 there is no separate MCP quota to check, and a 402 can just as easily be an agent as your own code.\n\n## Idempotency\n\nSend an `Idempotency-Key` header on `POST /extract`, `POST /assess`, `POST /verify`, `POST /verify/batch`, `POST /verify/{task_id}/select`, `POST /review`, `POST /citecheck` and `POST /ask/{verification_id}` so retries after a network drop do not spawn duplicate tasks or double-debit quota. The server caches the response body and returns it on replay; a re-used key with a different body returns 422. The SDKs auto-generate a key for `verify_and_wait`; on other calls pass `idempotency_key` yourself.\n\nOn `/ask` a key also keeps the retry out of the conversation: without one, a repeated question is a second turn, a second credit and a second answer that the next turn reads as context. A retry that lands while the first call is still running gets 409 rather than the reply \u2014 nothing is finalized yet to replay \u2014 so treat it as work already in progress and retry the same key once the first call has had time to finish. `GET /ask/{verification_id}` returns the message history, but carries no request id, so it can only tell you which answer was yours when one question is in flight at a time. The SDKs send the header when you pass a key yourself: `ask.send(..., idempotency_key=...)` in Python, `ask.send(id, { message, idempotencyKey })` in Node; neither generates one for ask, because a repeated question is usually deliberate. The n8n node's Ask operation sends a key scoped to the workflow execution, so a retry replays rather than re-asks. The Zapier app sends none on ask.\n\n## Errors\n\nThe status code says what to do next; the body says what happened. Every response carries an `X-Request-ID` header \u2014 quote it on a support ticket.\n\n| Status | Condition | Retry? |\n|---|---|---|\n| 401 | Missing, malformed or unknown key | After fixing credentials |\n| 402 | Out of credits, or the plan is the limit | No \u2014 billing action |\n| 403 | Authenticated but not allowed (private verification, IP block) | No |\n| 409 | A request with the same `Idempotency-Key` is still running, or the work the call names is not in the state it needs (`no_selection_pending`, `verification_not_ready`, `verification_failed`) | Same-key conflict: yes, with the same key. `verification_not_ready`: poll, or choose the claims when it needs input. `no_selection_pending`: poll the status first |\n| 422 | Malformed input | No |\n| 429 | Rate limit: the per-account `/extract` daily cap, or too many reviews or citation checks running (`review_in_flight`, `citecheck_in_flight`) | Yes, after `Retry-After` |\n| 502 | A sync step (`/assess`, `/extract`, `/ask`) failed deterministically \u2014 a schema or parse failure (`/ask`: `ask_failed`) | No \u2014 same input, same outcome |\n| 503 | Our model providers were rate-limited or down for the step and every fallback was exhausted (`code: upstream_unavailable`), the pipeline has no capacity (`code: capacity`), or citation checking is switched off (`/citecheck`, `code: citations_unavailable`) | Yes, after `Retry-After` (body `retry_after` too) |\n\n**402** carries `detail`, `code` (always `no_credits`), `upgrade_url`, `doc_url`, `wall_id`, `cost` (what the rejected call would have taken), and \u2014 when resolvable \u2014 `remaining`, `credits_remaining` and `resets_at`. `remaining` and `requested` are in the *capability's* unit (verifications, assesses); `credits_remaining` and `cost` are in credits, so a client can tell \"not enough for a 10-credit verify\" from \"empty\". The resolvable fields are omitted rather than sent as `null`, so an absent key means \"unknown\", not \"zero\".\n\n`wall_id` identifies this specific rejection and is already appended to `upgrade_url`, so sending a user to that URL is all that is needed \u2014 it exists as its own field only for clients that build their own upgrade link. Treat `upgrade_url` as opaque and follow it whole rather than matching it against a fixed string.\n\nThe `code` values are deliberately plan-agnostic: they name the condition, never the plan that would fix it. Send users to `upgrade_url` rather than mapping codes to plan names \u2014 the set of plans changes, the conditions do not.\n\nAn empty balance is deliberately **not** a 429: a 429 tells every client to retry, and an empty balance never succeeds on retry. **429** carries `code`, `limit`, `reset_in_seconds`, `upgrade_url` and the `Retry-After` header. The in-flight 429 of `/review` and `/citecheck` (`review_in_flight`, `citecheck_in_flight`) carries `retry_after_seconds` and `Retry-After` instead. The daily cap is the same on every plan; only a key Lenz has marked unlimited for `/extract` bypasses it.\n\n**Migrating (August 2026).** These rejections were 403, which the SDKs map to their auth error; they are now 402, which maps to the quota error \u2014 not a subclass. If you catch the auth error to handle an empty balance, that branch stops firing. This reaches you on every SDK version, including ones released before the change. `detail` strings are unchanged. Full reference: [lenz.io/docs/errors](https://lenz.io/docs/errors#quota).\n\n## Webhooks\n\nOn `POST /verify` and `POST /review` you may supply a `webhook_url`. We POST an HMAC-SHA256-signed JSON payload to it when the pipeline terminates. Signature header is `X-Lenz-Signature: sha256=`. Verify with your webhook secret (generate + rotate on [API credentials](https://lenz.io/api-credentials)). Retries: 10s, 60s, 600s (3 retries after the initial delivery, 4 attempts total). The SDKs ship a `LenzWebhooks` helper that verifies signatures and parses the payload into typed events. Events: `verification.completed`, `verification.failed`, `certificate.timestamped`, and for a review `review.completed` / `review.failed` (switch on `event`; a review's own deep checks fire no `verification.*` events).\n\nFor commercial use, volume pricing, or onboarding support, [get in touch](https://lenz.io/contact).", + "description": "# Four API calls and one to run them\n\nA research-depth ladder: find claims, judge fast, prove deep, citecheck; `/review` runs them all.\n\n- `POST /extract` \u2014 enumerate every major verifiable claim in a document (up to 100, most check-worthy first, inputs to 50k chars), or in a public web page given its URL; an optional `focus` narrows the list to the claims you care about, and `locate: true` keeps only the claims traced back to the text, with where each is made. Costs no credits; fair-use cap 1,000/account/day.\n- `POST /assess` \u2014 fast 3-model frontier panel verdict, typically 10-25s. Sync, 1 credit per claim. Send one text in `claim`, or a list of up to 20 in `claims` \u2014 one row per claim, same order, all checked in parallel. A text that makes more claims than one call checks gets the most check-worthy checked and the rest back as `more_claims`, free, to send on.\n- `POST /verify` \u2014 full multi-model pipeline with citations in ~90s. Async, 10 credits \u2014 or 5 with `depth: \"low\"`, a shallower check that comes back sooner.\n- `POST /citecheck` \u2014 does each cited source say what the text attributes to it: send pairs of a statement and its URL or DOI, or a whole text. Async; 1 credit per checked citation, refunded when a citation could not be checked ([guide](https://lenz.io/docs/citations)).\n- `POST /review` \u2014 the extract \u2192 assess \u2192 verify ladder on a whole draft, returning the issues with suggested rewrites in 2-4 min. Async, 1 credit per claim assessed + 10 (5 at `depth: \"low\"`) per claim verified. With `escalate.max_citations` (0-20, default 0) it also checks whether each source the draft cites says what the draft attributes to it, at 1 credit per checked citation (one that could not be checked is refunded) ([guide](https://lenz.io/docs/citations)). `more_claims` and `more_citations` list what the review found and did not check, at no cost; send them to `/assess` and `/citecheck` to check the rest.\n- Also `POST /ask/{id}` \u2014 ask follow-up questions grounded on a verification.\n\n## Built for teams whose AI output is async or document-shaped\n\nLegal-memo generators, AI deep-research, due-diligence platforms, vertical agents producing structured deliverables, \"report-as-a-service\" companies. `/assess` opens the door to sync UX too \u2014 fast enough to gate a chat completion or a UI submit.\n\n## API versions\n\nLenz versions the shape of its responses by date: send `X-Lenz-API-Version: 2026-10-11` to choose this shape. This reference describes `2026-10-11`. A field `2026-05-13` still sends under an older name is listed `deprecated` beside the current one, and where its shape differs, the operation names the `...Legacy` component it receives (`x-lenz-versions` on the response). Every response carries `X-Lenz-API-Version`, naming the version it is in. Which version a request gets without the header, and what changed: [https://lenz.io/docs/api-versions](https://lenz.io/docs/api-versions).\n\n## Install the SDK\n\n```bash\npip install lenz-io # Python\nnpm install lenz-io # Node / TypeScript\n```\n\nSource on GitHub: [lenz-io-python](https://github.com/lenzhq/lenz-io-python) \u00b7 [lenz-io-node](https://github.com/lenzhq/lenz-io-node).\n\n## The canonical integration: one /review call\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# Extract, assess, and verify the doubtful claims in one call (async, 2-4 min)\ndraft = \"The EU AI Act took effect in March 2024. It sorts AI systems into four risk tiers.\"\nreview = client.review_and_wait(text=draft)\nfor i in review.issues:\n print(i.verdict, i.claim, i.suggested_rewrite)\n```\n\nThe default policy: a quick verdict on every claim (up to 20), and the deep check on the rows whose quick verdict is `False`, `Mostly False` or `Mixed`, or whose confidence is `low`, up to five. Change it in `escalate`. To deep-check the rows the cap left out (`escalation.disposition == \"cap\"`), send them to `/verify/batch`. A resend with the same `Idempotency-Key` within 24 hours returns the same review; a new key is a new review.\n\n## The primitives, call by call: extract \u2192 assess \u2192 conditional verify\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# 1. Enumerate the factual claims in your model output (free)\n# extracted.claims lists every claim found, most check-worthy first:\n# always a list (one entry for one claim, [] for none).\n# Pass focus=\"market size and competitors\" to narrow the list;\n# status is \"no_match\" when the document has claims but none match.\nextracted = client.extract(text=llm_output)\nclaims = [c.claim for c in extracted.claims]\n\n# 2. Fast verdict in ONE call (typically 10-25s, 3-model panel): a list of up\n# to 20, one row per claim, in the order you sent them. extract finds up to\n# 100, most check-worthy first, so this checks the first 20.\nquick = client.assess(claims=claims[:20]).claims if claims else []\n\n# 3. Escalate the low-confidence ones to /verify in one batch (~90s, multi-model pipeline)\n# Rows we ran out of time on are free and worth resending; the rest of the\n# failed rows (no_checkable_claim, framing_failed) want a different input.\nretry = [c.claim for c in quick if c.failure and c.failure.code == \"timeout\"]\nquick += client.assess(claims=retry).claims if retry else []\ndoubtful = [{\"claim\": c.claim} for c in quick if c.status == \"completed\" and c.confidence == \"low\"]\nfor r in client.verify_batch_and_wait(claims=doubtful) if doubtful else []:\n if r.verification:\n print(r.verification.verdict, r.verification.lenz_score, r.verification.key_finding)\n```\n\n## How /verify works under the hood\n\nFrame \u2192 Collect Evidence \u2192 Debate (2 models, 2 rounds) \u2192 Adjudicate (three reviewers on models from different vendors, two more when they disagree) \u2192 Conclude. ~90 seconds wall-clock per claim.\n\n### While /verify is running\n\n`GET /verify/status/{task_id}` answers one of five shapes, keyed on `status`: `processing`, `needs_input`, `completed`, `failed`, and, after you cancel the run (see Stopping a run), `cancelled`. Every one echoes `task_id`, and only the fields belonging to that shape are present \u2014 read the ones for the branch you are in, and treat an absent key as absent rather than empty.\n\nA `processing` body carries `progress`:\n\n```json\n{\"status\": \"processing\", \"task_id\": \"\u2026\",\n \"progress\": {\"step\": \"research\", \"index\": 2, \"total\": 5,\n \"elapsed_seconds\": 42, \"poll_after_seconds\": 5}}\n```\n\n`step` is a closed set \u2014 `starting`, `framing`, `research`, `debate`, `adjudication`, `conclusion` \u2014 so it is safe to switch on. `index` is the 1-based stage position (0 while `starting`) out of `total`; read `total` off the response rather than hard-coding it. `poll_after_seconds` is how long to wait before looking again, and `elapsed_seconds` is how long the run has been going \u2014 a measurement, not an estimate of what is left.\n\nNote that `index` is stage **position**, not elapsed work: the stages are uneven, so a progress bar driven by it will sit on `research` for roughly half the run.\n\n`progress` is **advisory**. It says the work is pending and which stage it is on; it is not a place to read results from. The verdict arrives once, under `result`, when `status` is `completed`.\n\n### When /verify asks for input\n\n`/verify` is not only submit-and-poll. `GET /verify/status/{task_id}` can terminate at `needs_input` \u2014 the submitted text held several claims, or was too vague to check as written \u2014 and the task waits rather than guessing which claim you meant. Several claims in one sentence count the same as several sentences.\n\nResolve it with `POST /verify/{task_id}/select`, passing the exact wording of the claim(s) you want from the ones the status offered. Selection is **by text, not index**; anything that was not offered is rejected with a 422. Each selected claim fans out into its own pipeline, so the response carries one `task_id` per claim \u2014 poll each the way you would a fresh `/verify`.\n\nClaims returned by `/extract` skip this entirely: submitted verbatim, they are never bounced back for rephrasing.\n\n### Covered verification\n\nEvery verification a paid Pro or Scale account runs is put in front of a gate. The ones that qualify carry a contractual warranty, and every result tells you whether it qualified \u2014 a `coverage` block on the completed `/verify/status/{task_id}` body, on `GET /verifications/{id}` and on the `verification.completed` webhook.\n\nIt is on by default, with no request field to set; an account on Pro or Scale can turn certificates off on the API credentials page, for checks submitted from then on (reason `account`; a verification that already carries a certificate keeps it).\n\n`status` is `covered`, `pending_timestamp` or `uncovered`; `reasons` says why not, in a closed seven-value vocabulary. **If you publish automatically, key on the `certificate.timestamped` webhook, not `verification.completed`** \u2014 cover requires the qualified timestamp to precede what you publish, and `completed` fires while the qualified timestamp is still pending.\n\nFull wire format, the reason vocabulary and the offline verifier: [lenz.io/docs/coverage](https://lenz.io/docs/coverage). What is covered and what is not: [lenz.io/warranty](https://lenz.io/warranty).\n\n### When /verify fails\n\nA `failed` status (and the `verification.failed` webhook) carries two axes. `failure_reason` says **where** the pipeline stopped (`research_empty`, `conclusion_failed`, `task_stuck`, \u2026); on the webhook the same value rides in `error`. `failure_class` says **why**, and `retryable` is derived from it:\n\n| `failure_class` | Meaning | `retryable` |\n|---|---|---|\n| `upstream_unavailable` | Model or search providers were rate-limited or down, or the task was never run \u2014 resubmit after a short wait | `true` |\n| `insufficient_evidence` | We looked; the sources are not there | `false` |\n| `invalid_input` | Not a checkable claim, an unreadable URL | `false` |\n| `cancelled` | You stopped it with `POST /verify/{task_id}/cancel`; not charged | `false` |\n| `internal` | Anything else \u2014 retrying the same input will not help | `false` |\n\nBranch on `retryable`, not on `failure_reason`: the same reason can mean either. `failure_class` is a **closed set** \u2014 these five values are stable identifiers. `failure_reason` is an open, informational set (`task_error`, `task_stuck`, `not_a_claim`, \u2026 will appear). A failed body also carries `docs_url`, pointing at the explanation of its class. A failed `/verify` is not charged.\n\nIn the next API version a cancelled run is not a failure: `GET /verify/status/{task_id}` answers `{\"status\": \"cancelled\", \"task_id\": \"\u2026\"}`, and a `webhook_url` receives `verification.cancelled`. A request made in 2026-05-13 keeps `status: \"failed\"` with `failure_class: \"cancelled\"` and receives `verification.failed`.\n\n### Stopping a run\n\n`POST /verify/{task_id}/cancel` stops a verification that has not finished. A cancelled run is not charged and saves no verification; model work already under way is not billed to you, and the run stops at its next step. The call is safe to repeat and answers 200 in every state: `cancelled: true` with `status: \"cancelled\"`, or `cancelled: false` with `status: \"completed\"` (it finished first and was charged as usual) or `\"failed\"`. It needs no `Idempotency-Key`. An unknown task, another account's task and a task not submitted through the API answer 404 `not_found`; a task that is a `/review`'s deep check answers 409 `use_review_cancel`. Each item of a `/verify/batch` has its own `task_id`, so cancel per task; there is no call for a whole batch. `POST /reviews/{review_id}/cancel` and `POST /citechecks/{citecheck_id}/cancel` stop everything in a review or a citation check. They refund the quick checks not yet served and the citations not yet checked, and keep what was delivered charged.\n\nOne of those reasons comes from framing rather than from a pipeline step and carries `failure_class: invalid_input`: `not_a_claim` (the input is not a checkable statement, or is too vague to have a checkable reading). A vague input that has a likely reading is verified on it, and the result's `claim` is that reading. On the webhook the same value rides in `error`, as an ordinary `verification.failed` delivery; before 2026-09-03 it fired nothing at all, so a task submitted with a `webhook_url` could terminate in silence.\n\n### What may change without notice\n\nNew keys may be added to status and webhook bodies at any time \u2014 ignore keys you do not recognise. Fields documented as **advisory** \u2014 `progress` and everything inside it \u2014 may also change or disappear without a version bump. The verdict fields under `result` may not: those change only with a versioned, announced release.\n\n## Common patterns\n\n- **Runtime sync UX**: `POST /assess` on the user-facing path; escalate `confidence == \"low\"` claims to `/verify` in the background.\n- **Runtime webhook**: `POST /verify` with `webhook_url`. Lenz POSTs the typed payload when done. Best for document pipelines.\n- **Whole drafts**: `POST /review` with `webhook_url`; handle `review.completed` (a `ReviewWebhookPayload`, deduped on `event_id`) and read `review.issues[]` and `review.citation_issues[]`.\n- **CI / pre-release**: `/extract` + batch `/verify` your golden set on every deploy. The free `/extract` tier covers most of this. Claims returned by `/extract` verify cleanly \u2014 submitted verbatim to `/verify`, they are never bounced back for rephrasing.\n- **Incident triage**: paste the offending output into `/extract`, then `/verify` the wrong-looking claims. Get a citation trail to send back to the customer.\n\n## What's in the response\n\nUnified vocabulary across every claim-shaped response:\n\n- **`claim`** \u2014 the framed claim text.\n- **`verdict`** \u2014 `\"True\" | \"Mostly True\" | \"Mixed\" | \"Mostly False\" | \"False\" | \"Error\"`.\n- **`confidence`** \u2014 `\"high\" | \"medium\" | \"low\"`.\n- **`lenz_score`** \u2014 1\u201310 integer score (deep payloads).\n- **`key_finding`** \u2014 one declarative sentence stating the most important fact the analysis established. For a false claim it states the CORRECTED fact rather than restating the claim, so render it next to the verdict label, never as a standalone headline. `\"\"` on older verifications predating the field.\n- **`suggested_rewrite`** \u2014 a suggested rewrite of `claim` that the verification's findings support. It has not been verified itself: before using it, review it or run it through `/verify`. `null` for a true claim, when no correction is established, and on older verifications. On every verification, single or listed (`GET /verifications/{id}`, `/verify/status`, the `verification.completed` webhook, `GET /verifications`, `GET /library`). On `/assess` rows too when the request sets `suggest_rewrite`: written from the quick check's reasoning for a claim it found False or Mostly False with high confidence, at no extra credit; `null` otherwise, and on every row when not requested.\n- **`sources`** \u2014 evidence with title, URL and snippet: the sentence(s) quoted from the page, verbatim and in the page's own language (`/verify` only).\n- **`audit`** \u2014 adjudication reasoning, debate transcript, panel agreement (`/verify` only).\n\n## How the API is organized\n\n- **Extract** \u2014 pull verifiable claims out of text.\n- **Assess** \u2014 fast 3-model verdict.\n- **Verify** \u2014 deep multi-model pipeline: submit one claim or a batch, poll status, and resolve a `needs_input` interrupt by selecting claims.\n- **Verifications** \u2014 list, fetch, delete, and toggle visibility on stored verifications.\n- **Library** \u2014 browse the public catalog. No API key needed.\n- **Ask** \u2014 follow-up questions, 1 credit each.\n- **Account** \u2014 your credit balance and the price list.\n\n## Credits\n\nOne pool per account funds every billable call, at a fixed weight:\n\n| Endpoint | Credits |\n|---|---|\n| `POST /verify` (and `/verify/batch`, `/select`) | 10 per claim |\n| `POST /verify` with `depth: \"low\"` | 5 per claim |\n| `POST /assess` | 1 per claim \u2014 per row that carries a verdict; error rows and rows answered from the cache are free |\n| `POST /ask` | 1 |\n| `POST /extract` | 0 \u2014 free, fair-use capped |\n\n`depth: \"low\"` caps research breadth \u2014 fewer discovery queries, a hard extraction ceiling, no recovery tiers on the happy path \u2014 while every reasoning step runs unchanged. It is not a model downgrade. You are charged for the depth you REQUESTED. A `/verify/batch` may mix depths per item and is billed per item.\n\nAn answer served from the last hour's cache is free: a `/verify`, `/assess` or `/review` claim that gets back a verdict checked in the last hour costs nothing, so a tool that resends a request is not charged twice. The exception is a `/verify` that issues you a new warranty certificate, charged at the depth you requested.\n\n`GET /me/usage` reports the balance under `credits` and the weights under `costs`, with parameter-dependent prices such as `depth` nested under `cost_options`. In version `2026-05-13` the per-capability blocks beside them (`verify`, `assess`, `ask`) are **projections** of that one balance into each capability's own unit \u2014 how many of those calls the remaining credits would buy \u2014 not separate allowances. Spending on any one of them moves all of them.\n\n## Authentication\n\n1. [Create a Lenz account](https://lenz.io/auth).\n2. Generate an API key on the [API credentials](https://lenz.io/api-credentials) page.\n3. Send it on every request:\n ```\n Authorization: Bearer lenz_...\n ```\n\nOr skip steps 1\u20132: **[lenz.io/setup](https://lenz.io/setup)** issues a test key and generates setup instructions for your environment \u2014 MCP client, SDK or plain HTTP \u2014 that you paste into your coding agent to do the wiring.\n\n### One balance, whichever door you come through\n\nLenz also runs a remote MCP server at `https://lenz.io/mcp`, so an agent can fact-check inside a conversation. It is **not** part of this surface \u2014 there are no MCP endpoints below, and its tools are documented at [lenz.io/integrations/mcp-server](https://lenz.io/integrations/mcp-server).\n\nIt matters here for one reason: that server is a client of this API. It takes the same `lenz_...` key and forwards it to the endpoints below, so MCP traffic spends the same credits, counts against the same rate limits, and shows up in `GET /me/usage` \u2014 there is no separate MCP quota to check, and a 402 can just as easily be an agent as your own code.\n\n## Idempotency\n\nSend an `Idempotency-Key` header on `POST /extract`, `POST /assess`, `POST /verify`, `POST /verify/batch`, `POST /verify/{task_id}/select`, `POST /review`, `POST /citecheck` and `POST /ask/{verification_id}` so retries after a network drop do not spawn duplicate tasks or double-debit quota. The server caches the response body and returns it on replay; a re-used key with a different body returns 422. The SDKs generate a key for `extract`, `assess`, `select`, `verify`, `verify_and_wait`, `review` and `citecheck` and reuse it across their own retries; `verify_batch` and `ask.send` send one only when you pass `idempotency_key` yourself.\n\nOn `/ask` a key also keeps the retry out of the conversation: without one, a repeated question is a second turn, a second credit and a second answer that the next turn reads as context. A retry that lands while the first call is still running gets 409 rather than the reply \u2014 nothing is finalized yet to replay \u2014 so treat it as work already in progress and retry the same key once the first call has had time to finish. `GET /ask/{verification_id}` returns the message history, but carries no request id, so it can only tell you which answer was yours when one question is in flight at a time. The SDKs send the header when you pass a key yourself: `ask.send(..., idempotency_key=...)` in Python, `ask.send(id, { message, idempotencyKey })` in Node; neither generates one for ask, because a repeated question is usually deliberate. The n8n node's Ask operation sends a key scoped to the workflow execution, so a retry replays rather than re-asks. The Zapier app sends none on ask.\n\n## Errors\n\nThe status code says what to do next; the body says what happened. Every response carries an `X-Request-ID` header \u2014 quote it on a support ticket.\n\n| Status | Condition | Retry? |\n|---|---|---|\n| 401 | Missing, malformed or unknown key | After fixing credentials |\n| 402 | Out of credits, or the plan is the limit | No \u2014 billing action |\n| 403 | Authenticated but not allowed (private verification, IP block) | No |\n| 409 | A request with the same `Idempotency-Key` is still running, or the work the call names is not in the state it needs (`no_selection_pending`, `verification_not_ready`, `verification_failed`, `use_review_cancel`) | Same-key conflict: yes, with the same key. `verification_not_ready`: poll, or choose the claims when it needs input. `no_selection_pending`: poll the status first. `use_review_cancel`: cancel the review instead |\n| 422 | Malformed input | No |\n| 429 | Rate limit: the per-account `/extract` daily cap, or too many reviews or citation checks running (`review_in_flight`, `citecheck_in_flight`) | Yes, after `Retry-After` |\n| 502 | A sync step (`/assess`, `/extract`, `/ask`) failed deterministically \u2014 a schema or parse failure (`/ask`: `ask_failed`) | No \u2014 same input, same outcome |\n| 503 | Our model providers were rate-limited or down for the step and every fallback was exhausted (`code: upstream_unavailable`) or the pipeline has no capacity (`code: capacity`) | Yes, after `Retry-After` (body `retry_after` too) |\n\n**402** carries `detail`, `code` (always `no_credits`), `upgrade_url`, `doc_url`, `wall_id`, `cost` (what the rejected call would have taken), and \u2014 when resolvable \u2014 `remaining`, `credits_remaining` and `resets_at`. `remaining` and `requested` are in the *capability's* unit (verifications, assesses); `credits_remaining` and `cost` are in credits, so a client can tell \"not enough for a 10-credit verify\" from \"empty\". The resolvable fields are omitted rather than sent as `null`, so an absent key means \"unknown\", not \"zero\".\n\n`wall_id` identifies this specific rejection and is already appended to `upgrade_url`, so sending a user to that URL is all that is needed \u2014 it exists as its own field only for clients that build their own upgrade link. Treat `upgrade_url` as opaque and follow it whole rather than matching it against a fixed string.\n\nThe `code` values are deliberately plan-agnostic: they name the condition, never the plan that would fix it. Send users to `upgrade_url` rather than mapping codes to plan names \u2014 the set of plans changes, the conditions do not.\n\nAn empty balance is deliberately **not** a 429: a 429 tells every client to retry, and an empty balance never succeeds on retry. **429** carries `code`, `limit`, `reset_in_seconds`, `upgrade_url` and the `Retry-After` header. The in-flight 429 of `/review` and `/citecheck` (`review_in_flight`, `citecheck_in_flight`) carries `retry_after_seconds` and `Retry-After` instead. The daily cap is the same on every plan; only a key Lenz has marked unlimited for `/extract` bypasses it.\n\n**Migrating (August 2026).** These rejections were 403, which the SDKs map to their auth error; they are now 402, which maps to the quota error \u2014 not a subclass. If you catch the auth error to handle an empty balance, that branch stops firing. This reaches you on every SDK version, including ones released before the change. `detail` strings are unchanged. Full reference: [lenz.io/docs/errors](https://lenz.io/docs/errors#quota).\n\n## Webhooks\n\nOn `POST /verify` and `POST /review` you may supply a `webhook_url`. We POST an HMAC-SHA256-signed JSON payload to it when the pipeline terminates. Signature header is `X-Lenz-Signature: sha256=`. Verify with your webhook secret (generate + rotate on [API credentials](https://lenz.io/api-credentials)). Retries: 10s, 60s, 600s (3 retries after the initial delivery, 4 attempts total). The SDKs ship a `LenzWebhooks` helper that verifies signatures and parses the payload into typed events. Events: `verification.completed`, `verification.failed`, `verification.cancelled`, `certificate.timestamped`, and for a review `review.completed` / `review.failed` / `review.cancelled` and for a citation check `citecheck.completed` / `citecheck.failed` / `citecheck.cancelled` (switch on `event`; a review's own deep checks fire no `verification.*` events). The `*.cancelled` events fire once, when you cancel, in the next API version; a submission made in 2026-05-13 receives the matching `*.failed` event with `failure_class: \"cancelled\"` instead.\n\nFor commercial use, volume pricing, or onboarding support, [get in touch](https://lenz.io/contact).", "termsOfService": "https://lenz.io/terms" }, "paths": { @@ -37,12 +37,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -56,7 +56,7 @@ } } }, - "description": "Service discovery root \u2014 links to docs, OpenAPI spec, and endpoints.\n\nReturned at ``GET /api/v1/`` so curious humans and probing clients\nget a useful JSON payload instead of a 404.\n\n**Versions.** default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Service discovery root \u2014 links to docs, OpenAPI spec, and endpoints.\n\nReturned at ``GET /api/v1/`` so curious humans and probing clients\nget a useful JSON payload instead of a 404.\n\n**Versions.** default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Discovery" ] @@ -147,12 +147,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerificationListOutLegacy" + "$ref": "#/components/schemas/VerificationListOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/VerificationListOut" }, "2026-05-13": { @@ -170,12 +170,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorOutLegacy" + "$ref": "#/components/schemas/ValidationErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ValidationErrorOut" }, "2026-05-13": { @@ -193,12 +193,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -212,7 +212,7 @@ } } }, - "description": "Search and browse the public catalog of verified claims.\n\nReturns paginated results from the Lenz Library. Only publicly\npublished verifications are included.\n\n**Sort options:** `recent`, `most_true`, `most_untrue`,\n`relevance` (only when `search` is provided), and `random`.\n\n**`curated`** restricts results to one or more named curated collections,\ncomma-separated. Currently supported: `trivia` (the LLM-curated,\ntrivia-worthy pool behind the open-source quiz demo).\n\n**`verdict`** filters to one or more verdict labels, comma-separated\n(e.g. `True,False`). Labels: `True`, `Mostly True`, `Mixed`,\n`Mostly False`, `False`.\n\n**Versions.** 200: `VerificationListOutLegacy` (the next version: `VerificationListOut`); 422: `ValidationErrorOutLegacy` (the next version: `ValidationErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Search and browse the public catalog of verified claims.\n\nReturns paginated results from the Lenz Library. Only publicly\npublished verifications are included.\n\n**Sort options:** `recent`, `most_true`, `most_untrue`,\n`relevance` (only when `search` is provided), and `random`.\n\n**`curated`** restricts results to one or more named curated collections,\ncomma-separated. Currently supported: `trivia` (the LLM-curated,\ntrivia-worthy pool behind the open-source quiz demo).\n\n**`verdict`** filters to one or more verdict labels, comma-separated\n(e.g. `True,False`). Labels: `True`, `Mostly True`, `Mixed`,\n`Mostly False`, `False`.\n\n**Versions.** 200: `2026-10-11` receives `VerificationListOut`, `2026-05-13` receives `VerificationListOutLegacy`; 422: `2026-10-11` receives `ValidationErrorOut`, `2026-05-13` receives `ValidationErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Library" ], @@ -223,7 +223,7 @@ }, { "lang": "TypeScript", - "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz(); // no api_key needed\nconst page = await client.library.listLibrary({ page: 1, sort: \"recent\" });\npage.items.forEach(v => console.log(v.claim, \"\u2192\", v.verdict));\n" + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz(); // no api_key needed\nconst page = await client.library.list({ page: 1, sort: \"recent\" });\npage.items.forEach(v => console.log(v.claim, \"\u2192\", v.verdict));\n" } ] } @@ -243,12 +243,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerifyAcceptedOutLegacy" + "$ref": "#/components/schemas/VerifyAcceptedOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/VerifyAcceptedOut" }, "2026-05-13": { @@ -286,12 +286,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorOutLegacy" + "$ref": "#/components/schemas/ValidationErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ValidationErrorOut" }, "2026-05-13": { @@ -309,12 +309,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -328,7 +328,7 @@ } } }, - "description": "Submit a claim for verification.\n\nThe claim goes in ``claim``. (``text`` is accepted as an alias \u2014 a\ndocument is ``text``, a claim is ``claim``; ``POST /extract`` is the\nendpoint that takes a document.)\n\nReturns a ``task_id`` immediately. Poll ``GET /verify/status/{task_id}``\nor supply ``webhook_url`` in the body for an asynchronous callback when\nthe pipeline terminates.\n\n**Duplicate submissions are collapsed.** With no ``Idempotency-Key``\nheader, the request body itself is the key: resubmitting an identical\nbody within 10 minutes returns the original ``task_id`` instead of\nstarting a second run. To run the same text twice on purpose, send your\nown ``Idempotency-Key`` \u2014 a supplied key always wins.\n\n**``depth``** selects how much work the check does. ``standard``\n(default) is the full pipeline. ``low`` searches fewer sources, skips\nthe recovery fetch tiers and stops the debate after the opening\narguments (no rebuttal round), so it comes back sooner \u2014 with less\nevidence behind the verdict. Framing, the adjudication panel and the\nconclusion run unchanged, and every step runs the same models either\nway; ``low`` is a volume lever, not a model downgrade.\n\n``low`` costs **half the credits**, and you are charged for the depth you\nREQUESTED. A claim answered from a check made in the last hour is free\n(unless it issues you a new warranty certificate, which is charged at the\ndepth you requested). The completed result echoes the depth the verdict\nwas actually produced with, which is why that field can read ``standard``\non a ``low`` request: the echo describes the evidence.\n\n**Versions.** 202: `VerifyAcceptedOutLegacy` (the next version: `VerifyAcceptedOut`); 422: `ValidationErrorOutLegacy` (the next version: `ValidationErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Submit a claim for verification.\n\nThe claim goes in ``claim``. (``text`` is accepted as an alias \u2014 a\ndocument is ``text``, a claim is ``claim``; ``POST /extract`` is the\nendpoint that takes a document.)\n\nReturns a ``task_id`` immediately. Poll ``GET /verify/status/{task_id}``\nor supply ``webhook_url`` in the body for an asynchronous callback when\nthe pipeline terminates.\n\n**Duplicate submissions are collapsed.** With no ``Idempotency-Key``\nheader, the request body itself is the key: resubmitting an identical\nbody within 10 minutes returns the original ``task_id`` instead of\nstarting a second run. To run the same text twice on purpose, send your\nown ``Idempotency-Key`` \u2014 a supplied key always wins.\n\n**``depth``** selects how much work the check does. ``standard``\n(default) is the full pipeline. ``low`` searches fewer sources, skips\nthe recovery fetch tiers and stops the debate after the opening\narguments (no rebuttal round), so it comes back sooner \u2014 with less\nevidence behind the verdict. Framing, the adjudication panel and the\nconclusion run unchanged, and every step runs the same models either\nway; ``low`` is a volume lever, not a model downgrade.\n\n``low`` costs **half the credits**, and you are charged for the depth you\nREQUESTED. A claim answered from a check made in the last hour is free\n(unless it issues you a new warranty certificate, which is charged at the\ndepth you requested). The completed result echoes the depth the verdict\nwas actually produced with, which is why that field can read ``standard``\non a ``low`` request: the echo describes the evidence.\n\n**Versions.** 202: `2026-10-11` receives `VerifyAcceptedOut`, `2026-05-13` receives `VerifyAcceptedOutLegacy`; 422: `2026-10-11` receives `ValidationErrorOut`, `2026-05-13` receives `ValidationErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Verify" ], @@ -350,11 +350,11 @@ "x-codeSamples": [ { "lang": "Python", - "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nv = client.verify_and_wait(claim=\"Sharks don't get cancer\")\n# Add language=\"es\" for Spanish output (12 languages supported).\n# Add depth=\"low\" for a shallower check that returns sooner.\nprint(v.verdict, v.lenz_score)\n# false 2.0\n" + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nv = client.verify_and_wait(claim=\"Sharks don't get cancer\")\n# Add language=\"es\" for Spanish output (12 languages supported).\n# Add depth=\"low\" for a shallower check that returns sooner.\nprint(v.verdict, v.lenz_score)\n# False 2\n" }, { "lang": "TypeScript", - "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst v = await client.verifyAndWait({ claim: \"Sharks don't get cancer\" });\n// Add depth: \"low\" for a shallower check that returns sooner.\nconsole.log(v.verdict, v.lenz_score);\n// false 2.0\n" + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst v = await client.verifyAndWait({ claim: \"Sharks don't get cancer\" });\n// Add depth: \"low\" for a shallower check that returns sooner.\nconsole.log(v.verdict, v.lenz_score);\n// False 2\n" } ] } @@ -374,12 +374,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BatchAcceptedOutLegacy" + "$ref": "#/components/schemas/BatchAcceptedOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/BatchAcceptedOut" }, "2026-05-13": { @@ -397,12 +397,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorOutLegacy" + "$ref": "#/components/schemas/ValidationErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ValidationErrorOut" }, "2026-05-13": { @@ -420,12 +420,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -439,7 +439,7 @@ } } }, - "description": "Fan-out wrapper around /verify for multi-claim LLM responses.\n\nBody: ``{claims: [{text, source_url?, webhook_url?, visibility?, depth?},\n...]}`` capped at 20 items. Server pre-checks total credits, then fans out N\nindependent pipelines \u2014 each with its own ``task_id`` and lifecycle.\nEach claim's webhook (per-item override, or batch ``webhook_url``, or\nper-key default) fires when its pipeline terminates. ``batch_id`` is\npurely for client-side correlation; the server doesn't gate anything\non it.\n\nOn a mid-fan-out enqueue error, the response includes the partial\n``items`` list and a ``partial: true`` flag. Per-claim credit spend\nhappens at each claim's normal persist time \u2014 claims that never\nenqueue are never charged.\n\n``depth`` works like ``visibility``: a batch-wide default that any item\ncan override. See ``POST /verify`` for what the values mean.\n\nEach item carries its claim in ``claim`` (``text`` is accepted as an\nalias), plus the same optional fields as ``POST /verify``.\n\n**Versions.** 202: `BatchAcceptedOutLegacy` (the next version: `BatchAcceptedOut`); 422: `ValidationErrorOutLegacy` (the next version: `ValidationErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Fan-out wrapper around /verify for multi-claim LLM responses.\n\nBody: ``{claims: [{text, source_url?, webhook_url?, visibility?, depth?},\n...]}`` capped at 20 items. Server pre-checks total credits, then fans out N\nindependent pipelines \u2014 each with its own ``task_id`` and lifecycle.\nEach claim's webhook (per-item override, or batch ``webhook_url``, or\nper-key default) fires when its pipeline terminates. ``batch_id`` is\npurely for client-side correlation; the server doesn't gate anything\non it.\n\nOn a mid-fan-out enqueue error, the response includes the partial\n``items`` list and a ``partial: true`` flag. Per-claim credit spend\nhappens at each claim's normal persist time \u2014 claims that never\nenqueue are never charged.\n\n``depth`` works like ``visibility``: a batch-wide default that any item\ncan override. See ``POST /verify`` for what the values mean.\n\nEach item carries its claim in ``claim`` (``text`` is accepted as an\nalias), plus the same optional fields as ``POST /verify``.\n\n**Versions.** 202: `2026-10-11` receives `BatchAcceptedOut`, `2026-05-13` receives `BatchAcceptedOutLegacy`; 422: `2026-10-11` receives `ValidationErrorOut`, `2026-05-13` receives `ValidationErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Verify" ], @@ -461,11 +461,11 @@ "x-codeSamples": [ { "lang": "Python", - "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nbatch = client.verify_batch(claims=[\n {\"text\": \"The Eiffel Tower is in Berlin.\"},\n {\"text\": \"Water boils at 100\u00b0C at sea level.\"},\n])\nfor item in batch.items:\n print(item.task_id, item.claim_text)\n" + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nbatch = client.verify_batch(claims=[\n {\"text\": \"The Eiffel Tower is in Berlin.\"},\n {\"text\": \"Water boils at 100\u00b0C at sea level.\"},\n])\nfor item in batch.items:\n print(item.task_id, item.claim)\n" }, { "lang": "TypeScript", - "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst batch = await client.verifyBatch({\n claims: [\n { text: \"The Eiffel Tower is in Berlin.\" },\n { text: \"Water boils at 100\u00b0C at sea level.\" },\n ],\n});\nbatch.items.forEach(i => console.log(i.taskId, i.claimText));\n" + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst batch = await client.verifyBatch({\n claims: [\n { text: \"The Eiffel Tower is in Berlin.\" },\n { text: \"Water boils at 100\u00b0C at sea level.\" },\n ],\n});\nbatch.items.forEach(i => console.log(i.task_id, i.claim));\n" } ] } @@ -485,12 +485,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssessOutLegacy" + "$ref": "#/components/schemas/AssessOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/AssessOut" }, "2026-05-13": { @@ -508,12 +508,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorOutLegacy" + "$ref": "#/components/schemas/ValidationErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ValidationErrorOut" }, "2026-05-13": { @@ -531,12 +531,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -550,7 +550,7 @@ } } }, - "description": "Fast verdict via a 3-model panel (sync, ~10-25s).\n\nEscalation contract: a ``claims[].claim`` text POSTed back verbatim to\n``/verify`` within 24 hours is verified as-is \u2014 in normal operation it\ndoes not return ``needs_input`` and its wording is not rewritten.\n\nTwo input forms, one response shape::\n\n claim / text \u2500\u25ba ONE text. Lenz reads the claims in it, and each becomes a\n row, up to 20 a call; the rest go back, unchecked and\n free, in ``more_claims``.\n claims[] \u2500\u25ba a LIST of up to 20 items, each one claim. One row per item, same order.\n A compound item is assessed on its primary reading and\n the rest is listed in that row's ``more_claims``.\n\nThe list form is the batch rung of the extract \u2192 assess \u2192 verify ladder:\none ``/assess`` call answers up to 20 of ``/extract``'s claims (it finds up\nto 100) in about the time a single call takes, because every item is\nassessed in parallel.\n\nA verdict can be served from memory: a completed ``/verify`` of the exact\nsame claim text, or a prior ``/assess`` of it, within the last hour,\nwhichever account ran it \u2014 the verdict is a function of the input.\n``verification_url`` is set only when the caller could read that\nverification by id.\n\nBilling: 1 credit per row that carries a verdict; error rows are free; a\nduplicate item is a second row and a second credit (the panel runs once).\n\n**Versions.** 200: `AssessOutLegacy` (the next version: `AssessOut`); 422: `ValidationErrorOutLegacy` (the next version: `ValidationErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Fast verdict via a 3-model panel (sync, ~10-25s).\n\nEscalation contract: a ``claims[].claim`` text POSTed back verbatim to\n``/verify`` within 24 hours is verified as-is \u2014 in normal operation it\ndoes not return ``needs_input`` and its wording is not rewritten.\n\nTwo input forms, one response shape::\n\n claim / text \u2500\u25ba ONE text. Lenz reads the claims in it, and each becomes a\n row, up to 20 a call; the rest go back, unchecked and\n free, in ``more_claims``.\n claims[] \u2500\u25ba a LIST of up to 20 items, each one claim. One row per item, same order.\n A compound item is assessed on its primary reading and\n the rest is listed in that row's ``more_claims``.\n\nThe list form is the batch rung of the extract \u2192 assess \u2192 verify ladder:\none ``/assess`` call answers up to 20 of ``/extract``'s claims (it finds up\nto 100) in about the time a single call takes, because every item is\nassessed in parallel.\n\nA verdict can be served from memory: a completed ``/verify`` of the exact\nsame claim text, or a prior ``/assess`` of it, within the last hour,\nwhichever account ran it \u2014 the verdict is a function of the input.\n``verification_url`` is set only when the caller could read that\nverification by id.\n\nBilling: 1 credit per row that carries a verdict; error rows are free; a\nduplicate item is a second row and a second credit (the panel runs once).\n\n**Versions.** 200: `2026-10-11` receives `AssessOut`, `2026-05-13` receives `AssessOutLegacy`; 422: `2026-10-11` receives `ValidationErrorOut`, `2026-05-13` receives `ValidationErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Assess" ], @@ -572,11 +572,11 @@ "x-codeSamples": [ { "lang": "Python", - "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nout = client.assess(text=\"Coffee causes cancer and the earth is flat.\")\n# Add language=\"es\" for Spanish verdict text; verdict labels stay English.\nfor c in out.claims:\n print(c.claim, \"\u2192\", c.verdict, c.confidence)\n if c.rationale:\n print(\" \", c.rationale)\n\n# Several claims at once: one row per item, same order (up to 20).\nrows = client.assess(claims=[\"Coffee causes cancer.\", \"The earth is flat.\"]).claims\nfor c in rows:\n print(c.verdict if c.error_code is None else c.hint)\n if c.dissent:\n print(\" One reviewer disagreed:\", c.dissent)\n" + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nout = client.assess(claim=\"Coffee causes cancer and the earth is flat.\")\n# Add language=\"es\" for Spanish verdict text; verdict labels stay English.\nfor c in out.claims:\n print(c.claim, \"\u2192\", c.verdict, c.confidence)\n if c.rationale:\n print(\" \", c.rationale)\n\n# Several claims at once: one row per item, same order (up to 20).\nrows = client.assess(claims=[\"Coffee causes cancer.\", \"The earth is flat.\"]).claims\nfor c in rows:\n print(c.failure.hint if c.status == \"failed\" else c.verdict)\n if c.dissent:\n print(\" One reviewer disagreed:\", c.dissent)\n" }, { "lang": "TypeScript", - "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst out = await client.assess({\n text: \"Coffee causes cancer and the earth is flat.\",\n});\nout.claims.forEach(c => {\n console.log(c.claim, \"\u2192\", c.verdict, c.confidence);\n if (c.rationale) console.log(\" \", c.rationale);\n});\n\n// Several claims at once: one row per item, same order (up to 20).\nconst rows = (await client.assess({\n claims: [\"Coffee causes cancer.\", \"The earth is flat.\"],\n})).claims;\nrows.forEach(c => {\n console.log(c.error_code ? c.hint : c.verdict);\n if (c.dissent) console.log(\" One reviewer disagreed:\", c.dissent);\n});\n" + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst out = await client.assess({\n claim: \"Coffee causes cancer and the earth is flat.\",\n});\nout.claims.forEach(c => {\n console.log(c.claim, \"\u2192\", c.verdict, c.confidence);\n if (c.rationale) console.log(\" \", c.rationale);\n});\n\n// Several claims at once: one row per item, same order (up to 20).\nconst rows = (await client.assess({\n claims: [\"Coffee causes cancer.\", \"The earth is flat.\"],\n})).claims;\nrows.forEach(c => {\n console.log(c.status === \"failed\" ? c.failure?.hint : c.verdict);\n if (c.dissent) console.log(\" One reviewer disagreed:\", c.dissent);\n});\n" } ] } @@ -596,12 +596,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExtractOutLegacy" + "$ref": "#/components/schemas/ExtractOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ExtractOut" }, "2026-05-13": { @@ -619,12 +619,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorOutLegacy" + "$ref": "#/components/schemas/ValidationErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ValidationErrorOut" }, "2026-05-13": { @@ -642,12 +642,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -661,7 +661,7 @@ } } }, - "description": "Extract verifiable claims from arbitrary text.\n\nRuns the extraction step on its own \u2014 no research, no debate, no credit\ncharge. Enumerates every distinct major factual\nclaim in the input (up to 100), ordered most-check-worthy-first, in\n`claims` (one item for a single claim, `[]` for none). Claims returned\nhere verify cleanly when submitted verbatim to `POST /verify`.\n\nPass `focus` to narrow the result to the claims you care about \u2014 a short\ndescription such as `\"market size, growth and competitors\"`, at most 300\ncharacters. A focus selects from the claims the extractor found and\nnothing else: it cannot add a claim, reword one, reorder them, change the\nlanguage, or change what counts as a claim. Ordering stays\nmost-check-worthy-first.\n\n`status` is one of:\n\n- `ready` \u2014 claims were found (and, with a `focus`, at least one matched).\n- `no_checkable_claim` \u2014 no claim in the input could be checked against\n public evidence.\n- `no_match` \u2014 claims were found but none fall within your `focus`. The\n unfocused list is never substituted; widen the focus and call again.\n\n`text` can also be a single public web page URL (http or https, nothing\nelse in the field). Lenz reads the page, or a YouTube video's transcript,\nand extracts the claims from its first 50,000 characters. Pages behind a\nlogin (Facebook, Instagram, Threads, LinkedIn) can't be read. A URL call\ntypically takes 5-40 seconds, so give it a 90-second client timeout (SDK\n2.13 and later do). A page that cannot be read answers 502\n`extraction_failed`; when the services that read pages are down, 503\n`upstream_unavailable` with `Retry-After`. Neither uses a unit of the\ndaily cap.\n\nPass `locate: true` to keep only the claims that can be traced directly\nback to your text, and get each claim's `positions`: the passages that\nmake it, with `start` / `end` offsets into `text` as you\nsent it, in Unicode code points. A claim the text does not make, or makes\nwith a different figure, is left out, so the list says what the author\nwrote and nothing else. Offsets count code points, not UTF-16 units: in\nJavaScript slice with `Array.from(text).slice(start, end).join('')`.\nFor a URL the offsets are `null` and each position carries its passage.\nLocating adds a few seconds, never more than 20. If it cannot run,\nevery `positions` is `null` and the claims are returned unfiltered. `locate`\ndefaults to `false`.\n\nAccepts up to 50,000 characters (longer input is truncated). Free for\nAPI-key holders, capped at 1000 calls per ACCOUNT per day (resets at\n00:00 UTC) regardless of input length \u2014 a focused call costs the same one\nunit as an unfocused one. The cap is shared across all of the account's\nkeys.\n\n**Versions.** 200: `ExtractOutLegacy` (the next version: `ExtractOut`); 422: `ValidationErrorOutLegacy` (the next version: `ValidationErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Extract verifiable claims from arbitrary text.\n\nRuns the extraction step on its own \u2014 no research, no debate, no credit\ncharge. Enumerates every distinct major factual\nclaim in the input (up to 100), ordered most-check-worthy-first, in\n`claims` (one item for a single claim, `[]` for none). Claims returned\nhere verify cleanly when submitted verbatim to `POST /verify`.\n\nPass `focus` to narrow the result to the claims you care about \u2014 a short\ndescription such as `\"market size, growth and competitors\"`, at most 300\ncharacters. A focus selects from the claims the extractor found and\nnothing else: it cannot add a claim, reword one, reorder them, change the\nlanguage, or change what counts as a claim. Ordering stays\nmost-check-worthy-first.\n\n`status` is one of:\n\n- `ready` \u2014 claims were found (and, with a `focus`, at least one matched).\n- `no_checkable_claim` \u2014 no claim in the input could be checked against\n public evidence.\n- `no_match` \u2014 claims were found but none fall within your `focus`. The\n unfocused list is never substituted; widen the focus and call again.\n\n`text` can also be a single public web page URL (http or https, nothing\nelse in the field). Lenz reads the page, or a YouTube video's transcript,\nand extracts the claims from its first 50,000 characters. Pages behind a\nlogin (Facebook, Instagram, Threads, LinkedIn) can't be read. A URL call\ntypically takes 5-40 seconds, so give it a 90-second client timeout (SDK\n2.13 and later do). A page that cannot be read answers 502\n`extraction_failed`; when the services that read pages are down, 503\n`upstream_unavailable` with `Retry-After`. Neither uses a unit of the\ndaily cap.\n\nPass `locate: true` to keep only the claims that can be traced directly\nback to your text, and get each claim's `positions`: the passages that\nmake it, with `start` / `end` offsets into `text` as you\nsent it, in Unicode code points. A claim the text does not make, or makes\nwith a different figure, is left out, so the list says what the author\nwrote and nothing else. Offsets count code points, not UTF-16 units: in\nJavaScript slice with `Array.from(text).slice(start, end).join('')`.\nFor a URL the offsets are `null` and each position carries its passage.\nLocating adds a few seconds, never more than 20. If it cannot run,\nevery `positions` is `null` and the claims are returned unfiltered. `locate`\ndefaults to `false`.\n\nAccepts up to 50,000 characters (longer input is truncated). Free for\nAPI-key holders, capped at 1000 calls per ACCOUNT per day (resets at\n00:00 UTC) regardless of input length \u2014 a focused call costs the same one\nunit as an unfocused one. The cap is shared across all of the account's\nkeys.\n\n**Versions.** 200: `2026-10-11` receives `ExtractOut`, `2026-05-13` receives `ExtractOutLegacy`; 422: `2026-10-11` receives `ValidationErrorOut`, `2026-05-13` receives `ValidationErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Extract" ], @@ -683,11 +683,11 @@ "x-codeSamples": [ { "lang": "Python", - "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nclaims = client.extract(text=\"Coffee causes cancer and the earth is flat.\")\n# Add language=\"es\" to return extracted claims in Spanish.\nfor c in claims.identified_claims:\n print(c)\n" + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nout = client.extract(text=\"Coffee causes cancer and the earth is flat.\")\n# Add language=\"es\" to return extracted claims in Spanish.\nfor c in out.claims:\n print(c.claim)\n" }, { "lang": "TypeScript", - "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst out = await client.extract({\n text: \"Coffee causes cancer and the earth is flat.\",\n});\nout.identifiedClaims.forEach(c => console.log(c));\n" + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst out = await client.extract({\n text: \"Coffee causes cancer and the earth is flat.\",\n});\n(out.claims ?? []).forEach(c => console.log(c.claim));\n" } ] } @@ -716,12 +716,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BatchAcceptedOutLegacy" + "$ref": "#/components/schemas/BatchAcceptedOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/BatchAcceptedOut" }, "2026-05-13": { @@ -739,12 +739,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorOutLegacy" + "$ref": "#/components/schemas/ValidationErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ValidationErrorOut" }, "2026-05-13": { @@ -762,12 +762,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -781,7 +781,7 @@ } } }, - "description": "Resolve a multi-claim interrupt by selecting one or more of the offered\nclaims.\n\nEach selected claim fans out into its own independent pipeline (like\n``/verify/batch``), so the response is batch-shaped:\n``{batch_id, items: [{task_id, claim}], partial?}``. Poll each\n``task_id`` via ``GET /verify/status/{task_id}``.\n\nSend the chosen claims in ``claims`` (``texts`` is accepted as an alias).\nEvery selected claim must match one that was offered in the prior\n``needs_input`` response (server-validated against the recorded offer set);\narbitrary client-asserted text is rejected.\n\n**Versions.** 202: `BatchAcceptedOutLegacy` (the next version: `BatchAcceptedOut`); 422: `ValidationErrorOutLegacy` (the next version: `ValidationErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Resolve a multi-claim interrupt by selecting one or more of the offered\nclaims.\n\nEach selected claim fans out into its own independent pipeline (like\n``/verify/batch``), so the response is batch-shaped:\n``{batch_id, items: [{task_id, claim}], partial?}``. Poll each\n``task_id`` via ``GET /verify/status/{task_id}``.\n\nSend the chosen claims in ``claims`` (``texts`` is accepted as an alias).\nEvery selected claim must match one that was offered in the prior\n``needs_input`` response (server-validated against the recorded offer set);\narbitrary client-asserted text is rejected.\n\n**Versions.** 202: `2026-10-11` receives `BatchAcceptedOut`, `2026-05-13` receives `BatchAcceptedOutLegacy`; 422: `2026-10-11` receives `ValidationErrorOut`, `2026-05-13` receives `ValidationErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Verify" ], @@ -826,12 +826,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StatusOutLegacy" + "$ref": "#/components/schemas/StatusOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/StatusOut" }, "2026-05-13": { @@ -849,12 +849,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -868,7 +868,7 @@ } } }, - "description": "Poll processing status for a submitted verification.\n\nFour shapes keyed on ``status`` \u2014 ``processing``, ``needs_input``,\n``completed``, ``failed`` \u2014 and only the fields belonging to the current\none are present (see the response schema). A field that does not belong\nto the current shape is omitted rather than sent as null, so a client may\ntest for presence.\n\n**Versions.** 200: `StatusOutLegacy` (the next version: `StatusOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Poll processing status for a submitted verification.\n\nFour shapes keyed on ``status`` \u2014 ``processing``, ``needs_input``,\n``completed``, ``failed`` \u2014 and only the fields belonging to the current\none are present (see the response schema). A field that does not belong\nto the current shape is omitted rather than sent as null, so a client may\ntest for presence.\n\n**Versions.** 200: `2026-10-11` receives `StatusOut`, `2026-05-13` receives `StatusOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Verify" ], @@ -889,6 +889,83 @@ ] } }, + "/api/v1/verify/{task_id}/cancel": { + "post": { + "operationId": "cancel", + "summary": "Cancel Verification", + "parameters": [ + { + "in": "path", + "name": "task_id", + "schema": { + "title": "Task Id", + "type": "string" + }, + "required": true + }, + { + "$ref": "#/components/parameters/XLenzApiVersion" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/CancelOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/CancelOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + }, + "default": { + "description": "An error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/ErrorOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/ErrorOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + } + }, + "description": "Stop a verification that has not finished.\n\nA cancelled run is not charged and produces no verification: nothing is\nsaved. Work already under way when you cancel (a model call in flight) is\nnot billed to you, and the run itself stops at its next step. A run waiting\non `POST /verify/{task_id}/select` is cancelled too.\n\nThe call is safe to repeat and answers 200 whatever the state, so a client\nthat gives up never has to handle an error for losing a race:\n\n- `cancelled: true`, `status: \"cancelled\"`: the run is cancelled, by this\n call or an earlier one.\n- `cancelled: false`, `status: \"completed\"`: it finished first. The\n verification exists and was charged as usual.\n- `cancelled: false`, `status: \"failed\"`: it had already failed.\n\n`GET /verify/status/{task_id}` answers `cancelled` from then on, and a\n`webhook_url` receives `verification.cancelled` (a `2026-05-13` submission\nreceives `verification.failed` with `failure_class: \"cancelled\"`).\n\nAnother account's task, an unknown one, and a web task answer 404. A task\nthat is a `/review`'s deep check answers 409 `use_review_cancel`: cancel\nthe review (`POST /reviews/{review_id}/cancel`), which cancels everything in\nit.\n\n**Versions.** 200: `2026-10-11` receives `CancelOut`, `2026-05-13` receives `CancelOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", + "tags": [ + "Verify" + ], + "security": [ + { + "APIKeyBearerAuth": [] + } + ] + } + }, "/api/v1/me/usage": { "get": { "operationId": "getUsage", @@ -904,12 +981,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MeUsageOutLegacy" + "$ref": "#/components/schemas/MeUsageOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/MeUsageOut" }, "2026-05-13": { @@ -927,12 +1004,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -946,7 +1023,7 @@ } } }, - "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\nThe current shape is ``credits``, ``costs``, ``cost_options``, ``extract``,\n``plan``, ``plan_label`` and ``has_webhook_secret``. Callers on API version\n``2026-05-13`` also get the deprecated fields below, which are kept for\nthem: ``credits.bonus``, ``quota_resets_at`` (the same\nas ``credits.resets_at``) and the ``verify`` / ``ask`` / ``assess`` blocks.\n\n``credits.bonus`` (``2026-05-13`` only) is the deprecated old name of\n``credits.extra``, the same number. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15 (#664), while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated** (``2026-05-13`` only). ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nRead ``credits`` and ``costs`` instead; the blocks and the per-block\n``credits`` alias stay for ``2026-05-13`` callers. Derive them::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias, kept for the same callers.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.\n\n**Versions.** 200: `MeUsageOutLegacy` (the next version: `MeUsageOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\nThe current shape is ``credits``, ``costs``, ``cost_options``, ``extract``,\n``plan``, ``plan_label`` and ``has_webhook_secret``. Callers on API version\n``2026-05-13`` also get the deprecated fields below, which are kept for\nthem: ``credits.bonus``, ``quota_resets_at`` (the same\nas ``credits.resets_at``) and the ``verify`` / ``ask`` / ``assess`` blocks.\n\n``credits.bonus`` (``2026-05-13`` only) is the deprecated old name of\n``credits.extra``, the same number. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15 (#664), while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated** (``2026-05-13`` only). ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nRead ``credits`` and ``costs`` instead; the blocks and the per-block\n``credits`` alias stay for ``2026-05-13`` callers. Derive them::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias, kept for the same callers.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.\n\n**Versions.** 200: `2026-10-11` receives `MeUsageOut`, `2026-05-13` receives `MeUsageOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Account" ], @@ -958,11 +1035,11 @@ "x-codeSamples": [ { "lang": "Python", - "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nu = client.usage()\n# One credit pool funds every call; `costs` is the price list.\nprint(u.credits.remaining, \"credits left\", u.costs)\n# The same balance, projected into one capability's unit.\nprint(u.verify.remaining, \"verify calls left\")\nprint(u.assess.remaining, \"assess calls left\")\nprint(u.credits.extra, \"of them extra credits (grants and top-ups, never expiring)\")\n" + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nu = client.usage()\n# One credit pool funds every call; `costs` is the price list.\nprint(u.credits.remaining, \"credits left\", u.costs)\n# The same balance in one capability's unit: credits.remaining // costs[capability].\nprint(u.credits.remaining // u.costs[\"verify\"], \"verify calls left\")\nprint(u.credits.remaining // u.costs[\"assess\"], \"assess calls left\")\nprint(u.credits.extra, \"of them extra credits (grants and top-ups, never expiring)\")\n" }, { "lang": "TypeScript", - "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst u = await client.usage();\nconsole.log(`${u.credits.remaining} credits left`, u.costs);\nconsole.log(`${u.verify.remaining} verify calls left`);\nconsole.log(`${u.assess.remaining} assess calls left`);\n" + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst u = await client.usage();\nconsole.log(`${u.credits.remaining} credits left`, u.costs);\n// The same balance in one capability's unit: credits.remaining / costs[capability].\nconsole.log(`${Math.floor(u.credits.remaining / u.costs[\"verify\"])} verify calls left`);\nconsole.log(`${Math.floor(u.credits.remaining / u.costs[\"assess\"])} assess calls left`);\n" } ] } @@ -1002,12 +1079,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerificationListOutLegacy" + "$ref": "#/components/schemas/VerificationListOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/VerificationListOut" }, "2026-05-13": { @@ -1025,12 +1102,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorOutLegacy" + "$ref": "#/components/schemas/ValidationErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ValidationErrorOut" }, "2026-05-13": { @@ -1048,12 +1125,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -1067,7 +1144,7 @@ } } }, - "description": "List the authenticated key's verifications (all visibilities).\n\n**Versions.** 200: `VerificationListOutLegacy` (the next version: `VerificationListOut`); 422: `ValidationErrorOutLegacy` (the next version: `ValidationErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "List the authenticated key's verifications (all visibilities).\n\n**Versions.** 200: `2026-10-11` receives `VerificationListOut`, `2026-05-13` receives `VerificationListOutLegacy`; 422: `2026-10-11` receives `ValidationErrorOut`, `2026-05-13` receives `ValidationErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Verifications" ], @@ -1083,7 +1160,7 @@ }, { "lang": "TypeScript", - "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst page = await client.verifications.listVerifications({ page: 1 });\npage.items.forEach(v => console.log(v.verificationId, v.verdict));\n" + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst page = await client.verifications.list({ page: 1 });\npage.items.forEach(v => console.log(v.verification_id, v.verdict));\n" } ] } @@ -1112,12 +1189,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ClaimDetailOutLegacy" + "$ref": "#/components/schemas/ClaimDetailOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ClaimDetailOut" }, "2026-05-13": { @@ -1150,12 +1227,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerificationNotReadyOutLegacy" + "$ref": "#/components/schemas/VerificationNotReadyOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/VerificationNotReadyOut" }, "2026-05-13": { @@ -1188,12 +1265,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -1207,7 +1284,7 @@ } } }, - "description": "Retrieve the full verification report for a single claim.\n\nReturns the verdict, citations, and (under ``audit``) the panel\nreasoning, debate transcript, and assessments. Same shape as the\n/verify webhook payload and the deprecated /library/{id} endpoint\nthis merger replaces.\n\nAuthorization model (optional Bearer):\n- Anon callers see any listed public claim (``visibility='public'`` AND\n ``status`` in {pending, published}) PLUS any API ``unlisted`` claim\n (``visibility='public'``, ``status='hidden'``, ``source_channel='api'``):\n unlisted is readable by verification_id, just never surfaced in listings.\n- Authed callers additionally see their own claims regardless of\n visibility or status.\n- Private claims, and non-API hidden public claims (retracted / pre-screen),\n owned by someone else return 404.\n\nAlso accepts the **task_id** ``/verify`` returned, in place of a\nverification_id. A completed run reads exactly as its verification_id\nwould. A run that has not completed answers **409** with ``status``\n(``processing``, ``needs_input`` or ``failed``) and a ``hint``, plus the\nfailure fields when it failed; poll ``GET /verify/status/{task_id}`` for\nprogress. A task_id that is unknown, or not yours, answers 404 with a\n``hint`` at that endpoint.\n\n**Versions.** 200: `ClaimDetailOutLegacy` (the next version: `ClaimDetailOut`); 409: `VerificationNotReadyOutLegacy` (the next version: `VerificationNotReadyOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Retrieve the full verification report for a single claim.\n\nReturns the verdict, citations, and (under ``audit``) the panel\nreasoning, debate transcript, and assessments. Same shape as the\n/verify webhook payload and the deprecated /library/{id} endpoint\nthis merger replaces.\n\nAuthorization model (optional Bearer):\n- Anon callers see any listed public claim (``visibility='public'`` AND\n ``status`` in {pending, published}) PLUS any API ``unlisted`` claim\n (``visibility='public'``, ``status='hidden'``, ``source_channel='api'``):\n unlisted is readable by verification_id, just never surfaced in listings.\n- Authed callers additionally see their own claims regardless of\n visibility or status.\n- Private claims, and non-API hidden public claims (retracted / pre-screen),\n owned by someone else return 404.\n\nAlso accepts the **task_id** ``/verify`` returned, in place of a\nverification_id. A completed run reads exactly as its verification_id\nwould. A run that has not completed answers **409** with ``status``\n(``processing``, ``needs_input`` or ``failed``) and a ``hint``, plus the\nfailure fields when it failed; poll ``GET /verify/status/{task_id}`` for\nprogress. A task_id that is unknown, or not yours, answers 404 with a\n``hint`` at that endpoint.\n\n**Versions.** 200: `2026-10-11` receives `ClaimDetailOut`, `2026-05-13` receives `ClaimDetailOutLegacy`; 409: `2026-10-11` receives `VerificationNotReadyOut`, `2026-05-13` receives `VerificationNotReadyOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Verifications" ] @@ -1250,12 +1327,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -1269,7 +1346,7 @@ } } }, - "description": "Delete an owned verification.\n\nRemoves the verification, its analysis, evidence and follow-up chat, and\nthe text of each request that submitted it from your credit history and\nfrom our API-call log (on a batch, that item only). Not reached by a\ndeletion: copies in our caches, which expire within 24 hours, and our\nbackups, which age out within 60 days.\n\nDeleting a warranted verification keeps its certificate and warranty; the\ncertificate stays at its `certificate_url`.\n\n**Versions.** default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Delete an owned verification.\n\nRemoves the verification, its analysis, evidence and follow-up chat, and\nthe text of each request that submitted it from your credit history and\nfrom our API-call log (on a batch, that item only). Not reached by a\ndeletion: copies in our caches, which expire within 24 hours, and our\nbackups, which age out within 60 days.\n\nDeleting a warranted verification keeps its certificate and warranty; the\ncertificate stays at its `certificate_url`.\n\n**Versions.** default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Verifications" ], @@ -1319,12 +1396,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -1338,7 +1415,7 @@ } } }, - "description": "Download the warranty certificate for a covered verification.\n\nResolved by (verification, ACCOUNT), not by verification alone: one cached\nanalysis can have several holders, each with their own certificate, their\nown cap and their own link. Returning \"the certificate for this\nverification\" would hand one customer another customer's document.\n\nByte-identical to ``/certificate/.json``. A withdrawn\ncertificate is still served \u2014 it is the record of what was warranted, and\nthe withdrawal date is on it.\n\nServed whether or not new certificates are currently being issued: an\nissued certificate stands.\n\n**Versions.** default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Download the warranty certificate for a covered verification.\n\nResolved by (verification, ACCOUNT), not by verification alone: one cached\nanalysis can have several holders, each with their own certificate, their\nown cap and their own link. Returning \"the certificate for this\nverification\" would hand one customer another customer's document.\n\nByte-identical to ``/certificate/.json``. A withdrawn\ncertificate is still served \u2014 it is the record of what was warranted, and\nthe withdrawal date is on it.\n\nServed whether or not new certificates are currently being issued: an\nissued certificate stands.\n\n**Versions.** default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Verifications" ], @@ -1398,12 +1475,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorOutLegacy" + "$ref": "#/components/schemas/ValidationErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ValidationErrorOut" }, "2026-05-13": { @@ -1421,12 +1498,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -1440,7 +1517,7 @@ } } }, - "description": "List public verifications semantically related to this one.\n\nReturns the closest public-library claims by semantic similarity\n(cosine distance, 0 = identical). Excludes the verification itself,\nnear-duplicates (distance < 0.2), and editorially-hidden claims. Empty\nlist when the verification has not been indexed yet (that happens\nshortly after it completes) or when no claim is close enough.\n\nAuthorization model (optional Bearer) \u2014 identical to\n``GET /verifications/{verification_id}``: anon callers reach listed public claims plus\nAPI-origin unlisted ones; a valid key additionally unlocks the\ncaller's own claims. Results are always drawn from the public\nlibrary only, so keyless access exposes nothing new.\n``limit`` is clamped to [1, 10].\n\n**Versions.** 422: `ValidationErrorOutLegacy` (the next version: `ValidationErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "List public verifications semantically related to this one.\n\nReturns the closest public-library claims by semantic similarity\n(cosine distance, 0 = identical). Excludes the verification itself,\nnear-duplicates (distance < 0.2), and editorially-hidden claims. Empty\nlist when the verification has not been indexed yet (that happens\nshortly after it completes) or when no claim is close enough.\n\nAuthorization model (optional Bearer) \u2014 identical to\n``GET /verifications/{verification_id}``: anon callers reach listed public claims plus\nAPI-origin unlisted ones; a valid key additionally unlocks the\ncaller's own claims. Results are always drawn from the public\nlibrary only, so keyless access exposes nothing new.\n``limit`` is clamped to [1, 10].\n\n**Versions.** 422: `2026-10-11` receives `ValidationErrorOut`, `2026-05-13` receives `ValidationErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Verifications" ], @@ -1495,12 +1572,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -1514,7 +1591,7 @@ } } }, - "description": "Get ask history and remaining quota for a verification.\n\n**Versions.** default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Get ask history and remaining quota for a verification.\n\n**Versions.** default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Ask" ], @@ -1562,12 +1639,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -1581,7 +1658,7 @@ } } }, - "description": "Delete all ask messages for this user on a verification.\n\n**Versions.** default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Delete all ask messages for this user on a verification.\n\n**Versions.** default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Ask" ], @@ -1629,12 +1706,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorOutLegacy" + "$ref": "#/components/schemas/ValidationErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ValidationErrorOut" }, "2026-05-13": { @@ -1652,12 +1729,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -1671,7 +1748,7 @@ } } }, - "description": "Send a question and get an expert response.\n\nThe reply (``content`` field on the response) is plain text with a\nsmall markdown subset baked in by the chat model:\n\n- ``**bold**`` and ``*italic*``\n- ``- `` or ``* `` bullet lists\n- Blank-line paragraph breaks; single newlines inside a paragraph\n mean line break\n- ``[label](url)`` links, only ever to one of the verification's\n sources: any other link is reduced to its label before the reply\n is returned\n\nThe model only produces these \u2014 no headings, no tables, no code\nblocks. Pass the reply through any markdown library or display it\nverbatim.\n\nDocumented at https://lenz.io/docs/quickstart#ask-reply-format.\n\n**Versions.** 422: `ValidationErrorOutLegacy` (the next version: `ValidationErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Send a question and get an expert response.\n\nThe reply (``content`` field on the response) is plain text with a\nsmall markdown subset baked in by the chat model:\n\n- ``**bold**`` and ``*italic*``\n- ``- `` or ``* `` bullet lists\n- Blank-line paragraph breaks; single newlines inside a paragraph\n mean line break\n- ``[label](url)`` links, only ever to one of the verification's\n sources: any other link is reduced to its label before the reply\n is returned\n\nThe model only produces these \u2014 no headings, no tables, no code\nblocks. Pass the reply through any markdown library or display it\nverbatim.\n\nDocumented at https://lenz.io/docs/quickstart#ask-reply-format.\n\n**Versions.** 422: `2026-10-11` receives `ValidationErrorOut`, `2026-05-13` receives `ValidationErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Ask" ], @@ -1722,12 +1799,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReviewErrorOutLegacy" + "$ref": "#/components/schemas/ReviewErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ReviewErrorOut" }, "2026-05-13": { @@ -1745,12 +1822,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReviewErrorOutLegacy" + "$ref": "#/components/schemas/ReviewErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ReviewErrorOut" }, "2026-05-13": { @@ -1798,12 +1875,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReviewErrorOutLegacy" + "$ref": "#/components/schemas/ReviewErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ReviewErrorOut" }, "2026-05-13": { @@ -1821,12 +1898,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReviewErrorOutLegacy" + "$ref": "#/components/schemas/ReviewErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ReviewErrorOut" }, "2026-05-13": { @@ -1844,12 +1921,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -1863,7 +1940,7 @@ } } }, - "description": "Read a draft, quick-check every claim, deep-check the ones that look\nwrong or uncertain, and hand back the issues with the fixes.\n\nReturns 202 with a `review_id`; read the review at the `Location`\n(`GET /reviews/{review_id}`), or receive `review.completed` /\n`review.failed` at your webhook. A review takes two to four minutes.\n\nIt spends the credits the primitives spend: 1 per claim assessed, and 10\n(5 at `depth: low`) per deep check it runs. At most\n`min(claims, max_assessments) \u00d7 1 + max_verifications \u00d7 depth cost`;\nthe review reports what it charged.\n\nSend `escalate.max_citations: N` to also check the draft's first N\ncitations (markdown links, bare URLs, DOIs, `[n]` markers with a reference\nlist): for each, whether the source says what the draft attributes to it.\nOff by default; 1 credit per checked citation, and a source that could not\nbe checked is not charged. `POST /citecheck` is the same check alone.\nThe rows are in `citations[]`, the ones that do not hold up in\n`citation_issues[]`, and `outcome` reads `issues_found` for them.\n`escalate.max_assessments: 0` runs no quick check (a review of the\ncitations alone).\n\nOnly claims traced directly back to the draft are checked: a claim found\nnowhere in it is left out (as `/extract` does with `locate: true`). Each\nclaim row's `positions` says where the draft makes it, and\n`more_claim_locations` gives `{claim, positions}` for each of\n`more_claims`. Claim and citation positions share one shape, `{start, end,\ntext}`: `start` / `end` in Unicode code points of `text` as you sent it,\nhalf-open, and `text` the passage (`null` on a citation, whose row carries\nthe statement). For a URL draft `start` / `end` are `null` and `text`\ncarries the passage.\n\nA `text` that is one URL is read like `/extract` reads it and counts as\none `/extract` call: past the daily `/extract` limit it answers 429\n`extract_daily_limit`. The other 429 is `review_in_flight` (three reviews\nrunning on the account).\n\n**Versions.** 402: `ReviewErrorOutLegacy` (the next version: `ReviewErrorOut`); 409: `ReviewErrorOutLegacy` (the next version: `ReviewErrorOut`); 429: `ReviewErrorOutLegacy` (the next version: `ReviewErrorOut`); 503: `ReviewErrorOutLegacy` (the next version: `ReviewErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Read a draft, quick-check every claim, deep-check the ones that look\nwrong or uncertain, and hand back the issues with the fixes.\n\nReturns 202 with a `review_id`; read the review at the `Location`\n(`GET /reviews/{review_id}`), or receive `review.completed` /\n`review.failed` at your webhook. A review takes two to four minutes.\n\nIt spends the credits the primitives spend: 1 per claim assessed, and 10\n(5 at `depth: low`) per deep check it runs. At most\n`min(claims, max_assessments) \u00d7 1 + max_verifications \u00d7 depth cost`;\nthe review reports what it charged.\n\nSend `escalate.max_citations: N` to also check the draft's first N\ncitations (markdown links, bare URLs, DOIs, `[n]` markers with a reference\nlist): for each, whether the source says what the draft attributes to it.\nOff by default; 1 credit per checked citation, and a source that could not\nbe checked is not charged. `POST /citecheck` is the same check alone.\nThe rows are in `citations[]`, the ones that do not hold up in\n`citation_issues[]`, and `outcome` reads `issues_found` for them.\n`escalate.max_assessments: 0` runs no quick check (a review of the\ncitations alone).\n\nOnly claims traced directly back to the draft are checked: a claim found\nnowhere in it is left out (as `/extract` does with `locate: true`). Each\nclaim row's `positions` says where the draft makes it, and\n`more_claim_locations` gives `{claim, positions}` for each of\n`more_claims`. Claim and citation positions share one shape, `{start, end,\ntext}`: `start` / `end` in Unicode code points of `text` as you sent it,\nhalf-open, and `text` the passage (`null` on a citation, whose row carries\nthe statement). For a URL draft `start` / `end` are `null` and `text`\ncarries the passage.\n\nA `text` that is one URL is read like `/extract` reads it and counts as\none `/extract` call: past the daily `/extract` limit it answers 429\n`extract_daily_limit`. The other 429 is `review_in_flight` (three reviews\nrunning on the account).\n\n**Versions.** 402: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; 409: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; 429: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; 503: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Review" ], @@ -1886,7 +1963,7 @@ { "lang": "Shell", "label": "cURL", - "source": "curl -X POST https://lenz.io/api/v1/review \\\n -H \"Authorization: Bearer lenz_...\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Idempotency-Key: draft-42\" \\\n -d '{\"text\": \"The EU AI Act took effect in March 2024. ...\",\n \"escalate\": {\"max_verifications\": 5}}'\n# 202 {\"review_id\": \"a1b2c3d4\", \"status\": \"queued\"}\n# Location: /api/v1/reviews/a1b2c3d4\n" + "source": "curl -X POST https://lenz.io/api/v1/review \\\n -H \"Authorization: Bearer lenz_...\" \\\n -H \"X-Lenz-API-Version: 2026-10-11\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Idempotency-Key: draft-42\" \\\n -d '{\"text\": \"The EU AI Act took effect in March 2024. ...\",\n \"escalate\": {\"max_verifications\": 5}}'\n# 202 {\"review_id\": \"a1b2c3d4\", \"status\": \"queued\"}\n# Location: /api/v1/reviews/a1b2c3d4\n" } ] } @@ -1927,10 +2004,10 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ReviewFullOutLegacy" + "$ref": "#/components/schemas/ReviewFullOut" }, { - "$ref": "#/components/schemas/ReviewIssuesOutLegacy" + "$ref": "#/components/schemas/ReviewIssuesOut" } ], "title": "Response" @@ -1938,7 +2015,7 @@ } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "anyOf": [ { "$ref": "#/components/schemas/ReviewFullOut" @@ -1972,12 +2049,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReviewErrorOutLegacy" + "$ref": "#/components/schemas/ReviewErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ReviewErrorOut" }, "2026-05-13": { @@ -1995,12 +2072,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReviewErrorOutLegacy" + "$ref": "#/components/schemas/ReviewErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ReviewErrorOut" }, "2026-05-13": { @@ -2018,12 +2095,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReviewErrorOutLegacy" + "$ref": "#/components/schemas/ReviewErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ReviewErrorOut" }, "2026-05-13": { @@ -2056,12 +2133,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -2075,7 +2152,7 @@ } } }, - "description": "The review as it stands: quick verdicts at ~25-40 s, deep checks as each\nlands. `?view=issues` returns the same object without `claims[]`. Poll no\nfaster than `poll_after_seconds`; read `issues[]` at `completed`.\n\n**Versions.** 200: `ReviewFullOutLegacy | ReviewIssuesOutLegacy` (the next version: `ReviewFullOut | ReviewIssuesOut`); 403: `ReviewErrorOutLegacy` (the next version: `ReviewErrorOut`); 404: `ReviewErrorOutLegacy` (the next version: `ReviewErrorOut`); 410: `ReviewErrorOutLegacy` (the next version: `ReviewErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "The review as it stands: quick verdicts at ~25-40 s, deep checks as each\nlands. `?view=issues` returns the same object without `claims[]`. Poll no\nfaster than `poll_after_seconds`; read `issues[]` at `completed`.\n\n**Versions.** 200: `2026-10-11` receives `ReviewFullOut | ReviewIssuesOut`, `2026-05-13` receives `ReviewFullOutLegacy | ReviewIssuesOutLegacy`; 403: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; 404: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; 410: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Review" ], @@ -2088,7 +2165,153 @@ { "lang": "Shell", "label": "cURL", - "source": "curl \"https://lenz.io/api/v1/reviews/a1b2c3d4?view=issues\" \\\n -H \"Authorization: Bearer lenz_...\"\n# Poll every poll_after_seconds until status is completed or failed,\n# then read outcome and issues[] (verdict, key_finding, suggested_rewrite).\n" + "source": "curl \"https://lenz.io/api/v1/reviews/a1b2c3d4?view=issues\" \\\n -H \"X-Lenz-API-Version: 2026-10-11\" \\\n -H \"Authorization: Bearer lenz_...\"\n# Poll every poll_after_seconds until status is completed or failed,\n# then read outcome and issues[] (verdict, key_finding, suggested_rewrite).\n" + } + ] + } + }, + "/api/v1/reviews/{review_id}/cancel": { + "post": { + "operationId": "cancelReview", + "summary": "Cancel a review", + "parameters": [ + { + "in": "path", + "name": "review_id", + "schema": { + "title": "Review Id", + "type": "string" + }, + "required": true + }, + { + "$ref": "#/components/parameters/XLenzApiVersion" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewFullOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/ReviewFullOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/ReviewFullOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewErrorOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/ReviewErrorOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/ReviewErrorOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewErrorOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/ReviewErrorOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/ReviewErrorOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + }, + "410": { + "description": "Gone", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewErrorOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/ReviewErrorOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/ReviewErrorOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + }, + "default": { + "description": "An error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/ErrorOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/ErrorOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + } + }, + "description": "Stop a review: its quick checks, its deep checks and its citation\nchecks. Returns the review as it stands afterwards, `status: cancelled`.\n\nYou are charged only for what it delivered before the cancel: quick\nchecks it served, deep checks that finished, citations it checked. The\nrest is refunded or never charged. A review that had already finished is\nreturned unchanged (`completed` or `failed`), so the body always says\nwhat happened; cancelling again is safe. A review's deep check cannot be\ncancelled on its own (`POST /verify/{task_id}/cancel` answers 409\n`use_review_cancel`): cancel the review.\n\n**Versions.** 200: `2026-10-11` receives `ReviewFullOut`, `2026-05-13` receives `ReviewFullOutLegacy`; 403: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; 404: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; 410: `2026-10-11` receives `ReviewErrorOut`, `2026-05-13` receives `ReviewErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", + "tags": [ + "Review" + ], + "security": [ + { + "APIKeyBearerAuth": [] } ] } @@ -2123,12 +2346,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + "$ref": "#/components/schemas/CitecheckErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckErrorOut" }, "2026-05-13": { @@ -2146,12 +2369,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + "$ref": "#/components/schemas/CitecheckErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckErrorOut" }, "2026-05-13": { @@ -2184,12 +2407,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + "$ref": "#/components/schemas/CitecheckErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckErrorOut" }, "2026-05-13": { @@ -2207,12 +2430,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + "$ref": "#/components/schemas/CitecheckErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckErrorOut" }, "2026-05-13": { @@ -2230,12 +2453,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -2249,7 +2472,7 @@ } } }, - "description": "Check whether each cited source says what the draft attributes to it.\n\nSend `text` (a draft with its links: markdown links, bare URLs, `doi:` and\n`doi.org` forms, `[n]` markers with a reference list) or `pairs` (up to 20\nstatements, each with the `url` or `doi` it cites). Returns 202 with a\n`citecheck_id`; read the check at the `Location`\n(`GET /citechecks/{citecheck_id}`), or receive `citecheck.completed` /\n`citecheck.failed` at your webhook.\n\nIt costs 1 credit per checked citation: at most 1 credit per citation you\nsend (or `max_citations` for a text), and a citation that could not be\nchecked is refunded. `credits.charged` on the check says what it cost.\n\n**Versions.** 402: `CitecheckErrorOutLegacy` (the next version: `CitecheckErrorOut`); 409: `CitecheckErrorOutLegacy` (the next version: `CitecheckErrorOut`); 429: `CitecheckErrorOutLegacy` (the next version: `CitecheckErrorOut`); 503: `CitecheckErrorOutLegacy` (the next version: `CitecheckErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "Check whether each cited source says what the draft attributes to it.\n\nSend `text` (a draft with its links: markdown links, bare URLs, `doi:` and\n`doi.org` forms, `[n]` markers with a reference list) or `pairs` (up to 20\nstatements, each with the `url` or `doi` it cites). Returns 202 with a\n`citecheck_id`; read the check at the `Location`\n(`GET /citechecks/{citecheck_id}`), or receive `citecheck.completed` /\n`citecheck.failed` at your webhook.\n\nIt costs 1 credit per checked citation: at most 1 credit per citation you\nsend (or `max_citations` for a text), and a citation that could not be\nchecked is refunded. `credits.charged` on the check says what it cost.\n\n**Versions.** 402: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; 409: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; 429: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; 503: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Citecheck" ], @@ -2294,12 +2517,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckOutLegacy" + "$ref": "#/components/schemas/CitecheckOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckOut" }, "2026-05-13": { @@ -2317,12 +2540,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + "$ref": "#/components/schemas/CitecheckErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckErrorOut" }, "2026-05-13": { @@ -2340,12 +2563,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + "$ref": "#/components/schemas/CitecheckErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckErrorOut" }, "2026-05-13": { @@ -2363,12 +2586,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + "$ref": "#/components/schemas/CitecheckErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckErrorOut" }, "2026-05-13": { @@ -2386,12 +2609,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorOutLegacy" + "$ref": "#/components/schemas/ErrorOut" } } }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ErrorOut" }, "2026-05-13": { @@ -2405,7 +2628,7 @@ } } }, - "description": "The check as it stands. Poll no faster than `poll_after_seconds`; read\n`citation_issues[]` at `completed`.\n\n**Versions.** 200: `CitecheckOutLegacy` (the next version: `CitecheckOut`); 403: `CitecheckErrorOutLegacy` (the next version: `CitecheckErrorOut`); 404: `CitecheckErrorOutLegacy` (the next version: `CitecheckErrorOut`); 410: `CitecheckErrorOutLegacy` (the next version: `CitecheckErrorOut`); default: `ErrorOutLegacy` (the next version: `ErrorOut`).", + "description": "The check as it stands. Poll no faster than `poll_after_seconds`; read\n`citation_issues[]` at `completed`.\n\n**Versions.** 200: `2026-10-11` receives `CitecheckOut`, `2026-05-13` receives `CitecheckOutLegacy`; 403: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; 404: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; 410: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Citecheck" ], @@ -2415,54 +2638,200 @@ } ] } - } - }, - "components": { - "schemas": { - "EntityRef": { - "description": "An entity (person, place, organization, concept) referenced in the\nsubmitted claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42`` for Douglas Adams)\nwhen Lenz resolved the entity against its internal catalog; ``null``\notherwise. Customers can join on ``qid`` to their own Wikidata-indexed\ncorpus, or hit the Wikidata API directly for richer metadata.", - "properties": { - "name": { - "title": "Name", - "type": "string" + }, + "/api/v1/citechecks/{citecheck_id}/cancel": { + "post": { + "operationId": "cancelCitecheck", + "summary": "Cancel a citation check", + "parameters": [ + { + "in": "path", + "name": "citecheck_id", + "schema": { + "title": "Citecheck Id", + "type": "string" + }, + "required": true }, - "qid": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Qid" + { + "$ref": "#/components/parameters/XLenzApiVersion" } - }, - "required": [ - "name" ], - "title": "EntityRef", - "type": "object" - }, - "VerificationListItemOut": { - "description": "Single item in a list of verifications: ``GET /verifications``\n(authed-owner listing) and ``GET /library`` (public catalog), one shape.\n\nSlim shape: no ``url`` (customers reference claims by\n``verification_id``), no ``visibility``.", - "properties": { - "verification_id": { - "title": "Verification Id", - "type": "string" - }, - "claim": { - "title": "Claim", - "type": "string" - }, - "domain": { - "title": "Domain", - "type": "string" - }, - "entities": { - "default": [], - "items": { - "$ref": "#/components/schemas/EntityRef" + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CitecheckOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/CitecheckOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/CitecheckOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CitecheckErrorOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/CitecheckErrorOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CitecheckErrorOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/CitecheckErrorOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + }, + "410": { + "description": "Gone", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CitecheckErrorOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/CitecheckErrorOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/CitecheckErrorOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + }, + "default": { + "description": "An error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOut" + } + } + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/ErrorOut" + }, + "2026-05-13": { + "$ref": "#/components/schemas/ErrorOutLegacy" + } + }, + "headers": { + "X-Lenz-API-Version": { + "$ref": "#/components/headers/XLenzApiVersion" + } + } + } + }, + "description": "Stop a citation check. Returns the check as it stands afterwards,\n`status: cancelled`.\n\nYou are charged only for the citations it checked before the cancel; the\nrest are refunded. A check that had already finished is returned\nunchanged (`completed` or `failed`); cancelling again is safe.\n\n**Versions.** 200: `2026-10-11` receives `CitecheckOut`, `2026-05-13` receives `CitecheckOutLegacy`; 403: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; 404: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; 410: `2026-10-11` receives `CitecheckErrorOut`, `2026-05-13` receives `CitecheckErrorOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", + "tags": [ + "Citecheck" + ], + "security": [ + { + "APIKeyBearerAuth": [] + } + ] + } + } + }, + "components": { + "schemas": { + "EntityRef": { + "description": "An entity (person, place, organization, concept) referenced in the\nsubmitted claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42`` for Douglas Adams)\nwhen Lenz resolved the entity against its internal catalog; ``null``\notherwise. Customers can join on ``qid`` to their own Wikidata-indexed\ncorpus, or hit the Wikidata API directly for richer metadata.", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "qid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Qid" + } + }, + "required": [ + "name" + ], + "title": "EntityRef", + "type": "object" + }, + "VerificationListItemOut": { + "description": "Single item in a list of verifications: ``GET /verifications``\n(authed-owner listing) and ``GET /library`` (public catalog), one shape.\n\nSlim shape: no ``url`` (customers reference claims by\n``verification_id``), no ``visibility``.", + "properties": { + "verification_id": { + "title": "Verification Id", + "type": "string" + }, + "claim": { + "title": "Claim", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "entities": { + "default": [], + "items": { + "$ref": "#/components/schemas/EntityRef" }, "title": "Entities", "type": "array" @@ -3710,14 +4079,15 @@ "type": "object" }, "StatusOut": { - "description": "Body of ``GET /verify/status/{task_id}``.\n\nFour shapes keyed on ``status``, and only the fields belonging to the\ncurrent one are present \u2014 ``processing`` carries ``progress``,\n``needs_input`` carries ``reason``, ``claims`` and ``hint``,\n``completed`` carries ``result``, and ``failed`` carries ``failure``.\nBranch on ``status`` and read the fields for that branch; absent is\nmeaningful, so do not treat a missing key as an empty one.\n\n``task_id`` is echoed on every shape, so a caller polling several\nverifications in one loop can tell the replies apart without threading\nthe id through their own call site.", + "description": "Body of ``GET /verify/status/{task_id}``.\n\nFive shapes keyed on ``status``, and only the fields belonging to the\ncurrent one are present \u2014``processing`` carries ``progress``,\n``needs_input`` carries ``reason``, ``claims`` and ``hint``,\n``completed`` carries ``result``, ``failed`` carries ``failure``, and\n``cancelled`` (the run was stopped with\n``POST /verify/{task_id}/cancel``) carries nothing more: it was not\ncharged and has no verification.\nBranch on ``status`` and read the fields for that branch; absent is\nmeaningful, so do not treat a missing key as an empty one.\n\n``task_id`` is echoed on every shape, so a caller polling several\nverifications in one loop can tell the replies apart without threading\nthe id through their own call site.", "properties": { "status": { "enum": [ "processing", "needs_input", "completed", - "failed" + "failed", + "cancelled" ], "title": "Status", "type": "string" @@ -3801,6 +4171,37 @@ "title": "StatusOut", "type": "object" }, + "CancelOut": { + "description": "Body of ``POST /verify/{task_id}/cancel``.\n\n``cancelled`` is true when the run is cancelled after this call, whether\nthis call stopped it or an earlier one did (a repeat is safe). A run that\nhad already finished is not touched: ``cancelled`` is false and ``status``\nsays how it ended (``completed``: the verification was saved and charged\nas usual). ``status`` is the same word ``GET /verify/status/{task_id}``\nanswers at that moment (version ``2026-05-13`` reads a cancelled run as\n``failed``, with ``cancelled: true``).\n\nA cancelled run is not charged and has no verification; model work already\nunder way when it stopped is not billed to you. The run itself stops at its\nnext step.", + "properties": { + "task_id": { + "title": "Task Id", + "type": "string" + }, + "cancelled": { + "title": "Cancelled", + "type": "boolean" + }, + "status": { + "enum": [ + "processing", + "needs_input", + "completed", + "failed", + "cancelled" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "task_id", + "cancelled", + "status" + ], + "title": "CancelOut", + "type": "object" + }, "MeUsageCreditsOut": { "description": "The account's credit balance \u2014 the one pool every capability spends.\n\nTwo buckets: the monthly allowance for the current plan (resets at\n``resets_at``) and non-expiring ``extra`` credits from grants and top-ups,\nwhich are spent only once the allowance is gone. ``remaining`` is the sum\nand is what a call is checked against.\n\n``bonus`` is a DEPRECATED alias of ``extra`` (the balance's name until\n2026-09-15); read ``extra``. It stays for existing callers. It was renamed because the balance holds top-ups you paid for as\nwell as grants.", "properties": { @@ -5681,13 +6082,14 @@ "type": "string" }, "status": { - "description": "verifying: deep checks or citation checks are running.", + "description": "verifying: deep checks or citation checks are running. cancelled: you cancelled it (POST /reviews/{review_id}/cancel); only what it delivered before that is charged.", "enum": [ "queued", "assessing", "verifying", "completed", - "failed" + "failed", + "cancelled" ], "title": "Status", "type": "string" @@ -6081,13 +6483,14 @@ "type": "string" }, "status": { - "description": "verifying: deep checks or citation checks are running.", + "description": "verifying: deep checks or citation checks are running. cancelled: you cancelled it (POST /reviews/{review_id}/cancel); only what it delivered before that is charged.", "enum": [ "queued", "assessing", "verifying", "completed", - "failed" + "failed", + "cancelled" ], "title": "Status", "type": "string" @@ -7432,11 +7835,13 @@ "type": "string" }, "status": { + "description": "cancelled: you cancelled it (POST /citechecks/{citecheck_id}/cancel). Only citations it checked before that are charged.", "enum": [ "queued", "checking", "completed", - "failed" + "failed", + "cancelled" ], "title": "Status", "type": "string" @@ -7764,12 +8169,13 @@ "type": "object" }, "ReviewWebhookPayload": { - "description": "Body of ``review.completed`` / ``review.failed``.\n\n``event_id`` is stable per (review, event): dedupe on it (``attempt``\nchanges on retries). ``delivered_at`` is the attempt time. Version\n``2026-05-13`` also sends ``task_id``, an internal delivery identity that\nis not pollable on ``/verify/status``.", + "description": "Body of ``review.completed`` / ``review.failed`` / ``review.cancelled``.\n\n``event_id`` is stable per (review, event): dedupe on it (``attempt``\nchanges on retries). ``delivered_at`` is the attempt time. Version\n``2026-05-13`` also sends ``task_id``, an internal delivery identity that\nis not pollable on ``/verify/status``.", "properties": { "event": { "enum": [ "review.completed", - "review.failed" + "review.failed", + "review.cancelled" ], "title": "Event", "type": "string" @@ -7785,7 +8191,8 @@ "status": { "enum": [ "completed", - "failed" + "failed", + "cancelled" ], "title": "Status", "type": "string" @@ -7815,12 +8222,13 @@ "type": "object" }, "CitecheckWebhookPayload": { - "description": "Body of ``citecheck.completed`` / ``citecheck.failed``.\n\n``event_id`` is stable per (check, event): dedupe on it. Version\n``2026-05-13`` also sends ``task_id``, an internal delivery identity that\nis not pollable on ``/verify/status``.", + "description": "Body of ``citecheck.completed`` / ``citecheck.failed`` / ``citecheck.cancelled``.\n\n``event_id`` is stable per (check, event): dedupe on it. Version\n``2026-05-13`` also sends ``task_id``, an internal delivery identity that\nis not pollable on ``/verify/status``.", "properties": { "event": { "enum": [ "citecheck.completed", - "citecheck.failed" + "citecheck.failed", + "citecheck.cancelled" ], "title": "Event", "type": "string" @@ -7836,7 +8244,8 @@ "status": { "enum": [ "completed", - "failed" + "failed", + "cancelled" ], "title": "Status", "type": "string" @@ -9508,17 +9917,16 @@ "type": "object" }, "StatusOutLegacy": { - "description": "The poll as `2026-05-13` sends it: a failed run carries `error`, `failure_reason` (`not_a_claim` where the current version says `no_checkable_claim`), `failure_class`, `retryable`, `docs_url` and `hint` at the top level instead of `failure`; `needs_input` options carry `text`; a result carries `modified_at`.", + "description": "The poll as `2026-05-13` sends it: a failed run carries `error`, `failure_reason` (`not_a_claim` where the current version says `no_checkable_claim`), `failure_class`, `retryable`, `docs_url` and `hint` at the top level instead of `failure`; `needs_input` options carry `text`; a result carries `modified_at`; a cancelled run is `failed` with `failure_class: \"cancelled\"`.", "properties": { "status": { + "type": "string", "enum": [ "processing", "needs_input", "completed", "failed" - ], - "title": "Status", - "type": "string" + ] }, "task_id": { "title": "Task Id", @@ -9619,6 +10027,35 @@ "title": "StatusOutLegacy", "type": "object" }, + "CancelOutLegacy": { + "description": "The cancel response as `2026-05-13` sends it: a cancelled run is `status: \"failed\"` (with `cancelled: true`), the word its poll says.", + "properties": { + "task_id": { + "title": "Task Id", + "type": "string" + }, + "cancelled": { + "title": "Cancelled", + "type": "boolean" + }, + "status": { + "type": "string", + "enum": [ + "processing", + "needs_input", + "completed", + "failed" + ] + } + }, + "required": [ + "task_id", + "cancelled", + "status" + ], + "title": "CancelOutLegacy", + "type": "object" + }, "VerificationNotReadyOutLegacy": { "description": "The 409 as `2026-05-13` sends it: `failure_reason`, `failure_class`, `retryable` and `docs_url` at the top level instead of `failure`.", "properties": { @@ -10300,226 +10737,21 @@ "type": "object", "description": "A review's deep check as `2026-05-13` sends it: `modified_at` (the completion time, set only when it falls on a later UTC day than `created_at`) instead of `completed_at`." }, - "ReviewWebhookPayloadLegacy": { - "description": "A `review.*` webhook as `2026-05-13` sends it: `task_id` (an internal delivery id, not pollable) beside `review_id`, and the review in its older shape.", + "ReviewFullOutLegacy": { "properties": { - "event": { - "enum": [ - "review.completed", - "review.failed" - ], - "title": "Event", - "type": "string" - }, - "event_id": { - "title": "Event Id", - "type": "string" - }, "review_id": { "title": "Review Id", "type": "string" }, "status": { + "type": "string", "enum": [ + "queued", + "assessing", + "verifying", "completed", "failed" - ], - "title": "Status", - "type": "string" - }, - "review": { - "$ref": "#/components/schemas/ReviewFullOutLegacy" - }, - "attempt": { - "title": "Attempt", - "type": "integer" - }, - "delivered_at": { - "title": "Delivered At", - "type": "string" - }, - "task_id": { - "type": "string" - } - }, - "required": [ - "event", - "event_id", - "review_id", - "status", - "review", - "attempt", - "delivered_at" - ], - "title": "ReviewWebhookPayloadLegacy", - "type": "object" - }, - "CitecheckWebhookPayloadLegacy": { - "description": "A `citecheck.*` webhook as `2026-05-13` sends it: `task_id` (an internal delivery id, not pollable) beside `citecheck_id`, and the check in its older shape.", - "properties": { - "event": { - "enum": [ - "citecheck.completed", - "citecheck.failed" - ], - "title": "Event", - "type": "string" - }, - "event_id": { - "title": "Event Id", - "type": "string" - }, - "citecheck_id": { - "title": "Citecheck Id", - "type": "string" - }, - "status": { - "enum": [ - "completed", - "failed" - ], - "title": "Status", - "type": "string" - }, - "citecheck": { - "$ref": "#/components/schemas/CitecheckOutLegacy" - }, - "attempt": { - "title": "Attempt", - "type": "integer" - }, - "delivered_at": { - "title": "Delivered At", - "type": "string" - }, - "task_id": { - "type": "string" - } - }, - "required": [ - "event", - "event_id", - "citecheck_id", - "status", - "citecheck", - "attempt", - "delivered_at" - ], - "title": "CitecheckWebhookPayloadLegacy", - "type": "object" - }, - "MeUsageOutLegacy": { - "description": "`/me/usage` as `2026-05-13` sends it: the current body plus `quota_resets_at`, `credits.bonus` and the `verify` / `ask` / `assess` blocks, every one always present.", - "properties": { - "plan": { - "default": "free", - "title": "Plan", - "type": "string" - }, - "plan_label": { - "default": "Free", - "title": "Plan Label", - "type": "string" - }, - "quota_resets_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Quota Resets At" - }, - "credits": { - "$ref": "#/components/schemas/MeUsageCreditsOut" - }, - "costs": { - "additionalProperties": { - "type": "integer" - }, - "title": "Costs", - "type": "object" - }, - "cost_options": { - "additionalProperties": { - "additionalProperties": { - "additionalProperties": { - "type": "integer" - }, - "type": "object" - }, - "type": "object" - }, - "default": {}, - "title": "Cost Options", - "type": "object" - }, - "verify": { - "anyOf": [ - { - "$ref": "#/components/schemas/MeUsageQuotaOut" - }, - { - "type": "null" - } - ] - }, - "ask": { - "anyOf": [ - { - "$ref": "#/components/schemas/MeUsageQuotaOut" - }, - { - "type": "null" - } - ] - }, - "assess": { - "anyOf": [ - { - "$ref": "#/components/schemas/MeUsageQuotaOut" - }, - { - "type": "null" - } - ] - }, - "extract": { - "$ref": "#/components/schemas/MeUsageExtractOut" - }, - "has_webhook_secret": { - "default": false, - "title": "Has Webhook Secret", - "type": "boolean" - } - }, - "required": [ - "credits", - "costs", - "extract" - ], - "title": "MeUsageOutLegacy", - "type": "object" - }, - "ReviewFullOutLegacy": { - "properties": { - "review_id": { - "title": "Review Id", - "type": "string" - }, - "status": { - "description": "verifying: deep checks or citation checks are running.", - "enum": [ - "queued", - "assessing", - "verifying", - "completed", - "failed" - ], - "title": "Status", - "type": "string" + ] }, "outcome": { "anyOf": [ @@ -10704,26 +10936,47 @@ ], "title": "ReviewFullOutLegacy", "type": "object", - "description": "`ReviewFullOut` as `2026-05-13` sends it: the same fields, with the older shape of what it contains." + "description": "A review as `2026-05-13` sends it: a cancelled review is `failed` with a `cancelled` failure." }, - "ReviewIssueOutLegacy": { + "ReviewIssuesOutLegacy": { "properties": { - "claim_index": { - "title": "Claim Index", - "type": "integer" + "review_id": { + "title": "Review Id", + "type": "string" }, - "claim": { + "status": { + "type": "string", + "enum": [ + "queued", + "assessing", + "verifying", + "completed", + "failed" + ] + }, + "outcome": { "anyOf": [ { + "enum": [ + "clean", + "issues_found", + "incomplete", + "unchecked" + ], "type": "string" }, { "type": "null" } ], - "title": "Claim" + "description": "Null until terminal. clean covers the selected claims at the policy's chosen depth. issues_found with an empty issues[] means the rows are in citation_issues[].", + "title": "Outcome" }, - "verified_claim": { + "created_at": { + "title": "Created At", + "type": "string" + }, + "completed_at": { "anyOf": [ { "type": "string" @@ -10732,42 +10985,546 @@ "type": "null" } ], - "title": "Verified Claim" + "title": "Completed At" }, - "verdict": { - "enum": [ - "True", - "Mostly True", - "Mixed", - "Mostly False", - "False" - ], - "title": "Verdict", + "language": { + "title": "Language", "type": "string" }, - "confidence": { + "policy": { + "$ref": "#/components/schemas/ReviewPolicyOut" + }, + "summary": { + "$ref": "#/components/schemas/ReviewSummaryOutLegacy" + }, + "credits": { + "$ref": "#/components/schemas/ReviewCreditsOut" + }, + "poll_after_seconds": { "anyOf": [ { - "enum": [ - "low", - "medium", - "high" - ], - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Confidence" + "title": "Poll After Seconds" }, - "source": { - "enum": [ - "assessment", - "verification" - ], - "title": "Source", - "type": "string" + "issues": { + "items": { + "$ref": "#/components/schemas/ReviewIssueOutLegacy" + }, + "title": "Issues", + "type": "array" + }, + "failures": { + "items": { + "$ref": "#/components/schemas/ReviewFailedRowOutLegacy" + }, + "title": "Failures", + "type": "array" + }, + "citation_issues": { + "default": [], + "description": "Citations whose source does not say what the draft says, the most serious first; partly_supported rows are not issues and are only in citations[]. [] when the check was not asked.", + "items": { + "$ref": "#/components/schemas/ReviewCitationIssueOutLegacy" + }, + "title": "Citation Issues", + "type": "array" + }, + "citation_failures": { + "default": [], + "items": { + "$ref": "#/components/schemas/ReviewCitationFailedRowOutLegacy" + }, + "title": "Citation Failures", + "type": "array" + }, + "more_claims": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Claims found past max_assessments, in the draft's order: found but not checked; send them in a later request to check them. Null until the draft is read.", + "title": "More Claims" + }, + "more_claim_locations": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClaimLocationOut" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Where the draft makes each of `more_claims`, in the same order: `{claim, positions}`, the shape `/extract` returns with `locate: true` (see a claim row's `positions`). Null until the draft is read.", + "title": "More Claim Locations" + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ReviewMoreCitationOut" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Citations found past the ones selected (up to 100): found but not checked; send them in a later request to check them. Null until the draft is read.", + "title": "More Citations" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/components/schemas/FailureOutLegacy" + }, + { + "type": "null" + } + ] + }, + "view": { + "const": "issues", + "title": "View", + "type": "string" + } + }, + "required": [ + "review_id", + "status", + "outcome", + "created_at", + "completed_at", + "language", + "policy", + "summary", + "credits", + "poll_after_seconds", + "issues", + "failures", + "failure", + "view" + ], + "title": "ReviewIssuesOutLegacy", + "type": "object", + "description": "A review as `2026-05-13` sends it: a cancelled review is `failed` with a `cancelled` failure." + }, + "CitecheckOutLegacy": { + "properties": { + "citecheck_id": { + "title": "Citecheck Id", + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "queued", + "checking", + "completed", + "failed" + ] + }, + "outcome": { + "anyOf": [ + { + "enum": [ + "clean", + "issues_found", + "incomplete", + "unchecked" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Null until terminal. incomplete: a citation could not be checked for a reason of ours.", + "title": "Outcome" + }, + "created_at": { + "title": "Created At", + "type": "string" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Completed At" + }, + "language": { + "description": "The language the reasoning is written in, as the review envelope echoes it.", + "title": "Language", + "type": "string" + }, + "poll_after_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Poll After Seconds" + }, + "policy": { + "$ref": "#/components/schemas/CitecheckPolicyOut" + }, + "summary": { + "$ref": "#/components/schemas/CitecheckSummaryOutLegacy" + }, + "credits": { + "$ref": "#/components/schemas/ReviewCreditsOut" + }, + "citations": { + "default": [], + "items": { + "$ref": "#/components/schemas/ReviewCitationOutLegacy" + }, + "title": "Citations", + "type": "array" + }, + "citation_issues": { + "default": [], + "description": "Citations whose source does not say what the draft says, the most serious first; partly_supported rows are not issues and are only in citations[].", + "items": { + "$ref": "#/components/schemas/ReviewCitationIssueOutLegacy" + }, + "title": "Citation Issues", + "type": "array" + }, + "citation_failures": { + "default": [], + "items": { + "$ref": "#/components/schemas/ReviewCitationFailedRowOutLegacy" + }, + "title": "Citation Failures", + "type": "array" + }, + "more_citations": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ReviewMoreCitationOut" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Citations found in the text past the ones selected (up to 100): found but not checked; send them in a later request to check them. Null until the text is read; [] for pairs.", + "title": "More Citations" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/components/schemas/FailureOutLegacy" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "citecheck_id", + "status", + "outcome", + "created_at", + "completed_at", + "language", + "poll_after_seconds", + "policy", + "summary", + "credits", + "failure" + ], + "title": "CitecheckOutLegacy", + "type": "object", + "description": "A citation check as `2026-05-13` sends it: a cancelled check is `failed` with a `cancelled` failure." + }, + "ReviewWebhookPayloadLegacy": { + "description": "A `review.*` webhook as `2026-05-13` sends it: `task_id` (an internal delivery id, not pollable) beside `review_id`, and the review in its older shape; a cancelled review is `review.failed`.", + "properties": { + "event": { + "type": "string", + "enum": [ + "review.completed", + "review.failed" + ] + }, + "event_id": { + "title": "Event Id", + "type": "string" + }, + "review_id": { + "title": "Review Id", + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "completed", + "failed" + ] + }, + "review": { + "$ref": "#/components/schemas/ReviewFullOutLegacy" + }, + "attempt": { + "title": "Attempt", + "type": "integer" + }, + "delivered_at": { + "title": "Delivered At", + "type": "string" + }, + "task_id": { + "type": "string" + } + }, + "required": [ + "event", + "event_id", + "review_id", + "status", + "review", + "attempt", + "delivered_at" + ], + "title": "ReviewWebhookPayloadLegacy", + "type": "object" + }, + "CitecheckWebhookPayloadLegacy": { + "description": "A `citecheck.*` webhook as `2026-05-13` sends it: `task_id` (an internal delivery id, not pollable) beside `citecheck_id`, and the check in its older shape; a cancelled check is `citecheck.failed`.", + "properties": { + "event": { + "type": "string", + "enum": [ + "citecheck.completed", + "citecheck.failed" + ] + }, + "event_id": { + "title": "Event Id", + "type": "string" + }, + "citecheck_id": { + "title": "Citecheck Id", + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "completed", + "failed" + ] + }, + "citecheck": { + "$ref": "#/components/schemas/CitecheckOutLegacy" + }, + "attempt": { + "title": "Attempt", + "type": "integer" + }, + "delivered_at": { + "title": "Delivered At", + "type": "string" + }, + "task_id": { + "type": "string" + } + }, + "required": [ + "event", + "event_id", + "citecheck_id", + "status", + "citecheck", + "attempt", + "delivered_at" + ], + "title": "CitecheckWebhookPayloadLegacy", + "type": "object" + }, + "MeUsageOutLegacy": { + "description": "`/me/usage` as `2026-05-13` sends it: the current body plus `quota_resets_at`, `credits.bonus` and the `verify` / `ask` / `assess` blocks, every one always present.", + "properties": { + "plan": { + "default": "free", + "title": "Plan", + "type": "string" + }, + "plan_label": { + "default": "Free", + "title": "Plan Label", + "type": "string" + }, + "quota_resets_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Quota Resets At" + }, + "credits": { + "$ref": "#/components/schemas/MeUsageCreditsOut" + }, + "costs": { + "additionalProperties": { + "type": "integer" + }, + "title": "Costs", + "type": "object" + }, + "cost_options": { + "additionalProperties": { + "additionalProperties": { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + "type": "object" + }, + "default": {}, + "title": "Cost Options", + "type": "object" + }, + "verify": { + "anyOf": [ + { + "$ref": "#/components/schemas/MeUsageQuotaOut" + }, + { + "type": "null" + } + ] + }, + "ask": { + "anyOf": [ + { + "$ref": "#/components/schemas/MeUsageQuotaOut" + }, + { + "type": "null" + } + ] + }, + "assess": { + "anyOf": [ + { + "$ref": "#/components/schemas/MeUsageQuotaOut" + }, + { + "type": "null" + } + ] + }, + "extract": { + "$ref": "#/components/schemas/MeUsageExtractOut" + }, + "has_webhook_secret": { + "default": false, + "title": "Has Webhook Secret", + "type": "boolean" + } + }, + "required": [ + "credits", + "costs", + "extract" + ], + "title": "MeUsageOutLegacy", + "type": "object" + }, + "ReviewIssueOutLegacy": { + "properties": { + "claim_index": { + "title": "Claim Index", + "type": "integer" + }, + "claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Claim" + }, + "verified_claim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Verified Claim" + }, + "verdict": { + "enum": [ + "True", + "Mostly True", + "Mixed", + "Mostly False", + "False" + ], + "title": "Verdict", + "type": "string" + }, + "confidence": { + "anyOf": [ + { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Confidence" + }, + "source": { + "enum": [ + "assessment", + "verification" + ], + "title": "Source", + "type": "string" }, "verification_id": { "anyOf": [ @@ -11291,247 +12048,19 @@ "index": { "title": "Index", "type": "integer" - }, - "reference": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Reference" - }, - "cited_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cited Url" - }, - "doi": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Doi" - }, - "statement": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Statement" - }, - "quotes": { - "default": [], - "items": { - "type": "string" - }, - "title": "Quotes", - "type": "array" - }, - "position": { - "anyOf": [ - { - "$ref": "#/components/schemas/PositionOut" - }, - { - "type": "null" - } - ] - }, - "result": { - "anyOf": [ - { - "$ref": "#/components/schemas/ReviewCitationResultOut" - }, - { - "type": "null" - } - ] - }, - "check": { - "$ref": "#/components/schemas/ReviewCitationCheckOutLegacy" - } - }, - "required": [ - "index", - "reference", - "cited_url", - "doi", - "statement", - "position", - "result", - "check" - ], - "title": "ReviewCitationOutLegacy", - "type": "object", - "description": "`ReviewCitationOut` as `2026-05-13` sends it: the same fields, with the older shape of what it contains." - }, - "ReviewCitationCheckOutLegacy": { - "properties": { - "status": { - "enum": [ - "pending", - "running", - "completed", - "failed" - ], - "title": "Status", - "type": "string" - }, - "page_read": { - "anyOf": [ - { - "enum": [ - "full", - "partial", - "not_found", - "none" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "description": "What reading the source gave: full, partial (cut short, a teaser or an abstract alone), not_found, none.", - "title": "Page Read" - }, - "page_title": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Page Title" - }, - "page_published_date": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Page Published Date" - }, - "page_language": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Page Language" - }, - "source_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Where the text was actually read from.", - "title": "Source Url" - }, - "source_version": { - "anyOf": [ - { - "enum": [ - "published", - "accepted", - "submitted" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Source Version" - }, - "support": { - "anyOf": [ - { - "enum": [ - "supported", - "partly_supported", - "contradicted", - "not_in_source", - "unchecked" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Support" - }, - "snippet": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "The passage from the source the support answer rests on, checked against the page text.", - "title": "Snippet" - }, - "rationale": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "The reviewer's sentence: reasoning, not a checked source.", - "title": "Rationale" - }, - "quote": { - "anyOf": [ - { - "enum": [ - "matched", - "not_in_source", - "unchecked" - ], + }, + "reference": { + "anyOf": [ + { "type": "string" }, { "type": "null" } ], - "title": "Quote" + "title": "Reference" }, - "missing_quote": { + "cited_url": { "anyOf": [ { "type": "string" @@ -11540,119 +12069,96 @@ "type": "null" } ], - "description": "The first of quotes the source does not contain; null unless quote is not_in_source.", - "title": "Missing Quote" + "title": "Cited Url" }, - "doi_registered": { + "doi": { "anyOf": [ { - "type": "boolean" + "type": "string" }, { "type": "null" } ], - "title": "Doi Registered" + "title": "Doi" }, - "metadata": { + "statement": { "anyOf": [ { - "enum": [ - "consistent", - "mismatch", - "unchecked" - ], "type": "string" }, { "type": "null" } ], - "title": "Metadata" + "title": "Statement" }, - "metadata_differences": { + "quotes": { "default": [], "items": { - "$ref": "#/components/schemas/ReviewMetadataDifferenceOut" + "type": "string" }, - "title": "Metadata Differences", + "title": "Quotes", "type": "array" }, - "registered": { + "position": { "anyOf": [ { - "$ref": "#/components/schemas/ReviewRegisteredOut" + "$ref": "#/components/schemas/PositionOut" }, { "type": "null" } ] }, - "unchecked_reason": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Why the finding is unchecked; an extensible string.", - "title": "Unchecked Reason" - }, - "hint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Hint" - }, - "failure": { + "result": { "anyOf": [ { - "$ref": "#/components/schemas/FailureOutLegacy" + "$ref": "#/components/schemas/ReviewCitationResultOut" }, { "type": "null" } ] + }, + "check": { + "$ref": "#/components/schemas/ReviewCitationCheckOutLegacy" } }, "required": [ - "status" + "index", + "reference", + "cited_url", + "doi", + "statement", + "position", + "result", + "check" ], - "title": "ReviewCitationCheckOutLegacy", + "title": "ReviewCitationOutLegacy", "type": "object", - "description": "`ReviewCitationCheckOut` as `2026-05-13` sends it: the same fields, with the older shape of what it contains." + "description": "`ReviewCitationOut` as `2026-05-13` sends it: the same fields, with the older shape of what it contains." }, - "CitecheckOutLegacy": { + "ReviewCitationCheckOutLegacy": { "properties": { - "citecheck_id": { - "title": "Citecheck Id", - "type": "string" - }, "status": { "enum": [ - "queued", - "checking", + "pending", + "running", "completed", "failed" ], "title": "Status", "type": "string" }, - "outcome": { + "page_read": { "anyOf": [ { "enum": [ - "clean", - "issues_found", - "incomplete", - "unchecked" + "full", + "partial", + "not_found", + "none" ], "type": "string" }, @@ -11660,14 +12166,10 @@ "type": "null" } ], - "description": "Null until terminal. incomplete: a citation could not be checked for a reason of ours.", - "title": "Outcome" - }, - "created_at": { - "title": "Created At", - "type": "string" + "description": "What reading the source gave: full, partial (cut short, a teaser or an abstract alone), not_found, none.", + "title": "Page Read" }, - "completed_at": { + "page_title": { "anyOf": [ { "type": "string" @@ -11676,150 +12178,106 @@ "type": "null" } ], - "title": "Completed At" - }, - "language": { - "description": "The language the reasoning is written in, as the review envelope echoes it.", - "title": "Language", - "type": "string" + "title": "Page Title" }, - "poll_after_seconds": { + "page_published_date": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Poll After Seconds" - }, - "policy": { - "$ref": "#/components/schemas/CitecheckPolicyOut" - }, - "summary": { - "$ref": "#/components/schemas/CitecheckSummaryOutLegacy" - }, - "credits": { - "$ref": "#/components/schemas/ReviewCreditsOut" - }, - "citations": { - "default": [], - "items": { - "$ref": "#/components/schemas/ReviewCitationOutLegacy" - }, - "title": "Citations", - "type": "array" - }, - "citation_issues": { - "default": [], - "description": "Citations whose source does not say what the draft says, the most serious first; partly_supported rows are not issues and are only in citations[].", - "items": { - "$ref": "#/components/schemas/ReviewCitationIssueOutLegacy" - }, - "title": "Citation Issues", - "type": "array" + "title": "Page Published Date" }, - "citation_failures": { - "default": [], - "items": { - "$ref": "#/components/schemas/ReviewCitationFailedRowOutLegacy" - }, - "title": "Citation Failures", - "type": "array" + "page_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Page Language" }, - "more_citations": { + "source_url": { "anyOf": [ { - "items": { - "$ref": "#/components/schemas/ReviewMoreCitationOut" - }, - "type": "array" + "type": "string" }, { "type": "null" } ], - "description": "Citations found in the text past the ones selected (up to 100): found but not checked; send them in a later request to check them. Null until the text is read; [] for pairs.", - "title": "More Citations" + "description": "Where the text was actually read from.", + "title": "Source Url" }, - "failure": { + "source_version": { "anyOf": [ { - "$ref": "#/components/schemas/FailureOutLegacy" + "enum": [ + "published", + "accepted", + "submitted" + ], + "type": "string" }, { "type": "null" } - ] - } - }, - "required": [ - "citecheck_id", - "status", - "outcome", - "created_at", - "completed_at", - "language", - "poll_after_seconds", - "policy", - "summary", - "credits", - "failure" - ], - "title": "CitecheckOutLegacy", - "type": "object", - "description": "`CitecheckOut` as `2026-05-13` sends it: the same fields, with the older shape of what it contains." - }, - "BatchAcceptedOutLegacy": { - "title": "BatchAcceptedOutLegacy", - "type": "object", - "description": "`BatchAcceptedOut` as `2026-05-13` sends it: the same fields, with the older shape of what it contains.", - "properties": { - "batch_id": { - "type": "string" + ], + "title": "Source Version" }, - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BatchAcceptedItemOutLegacy" - } + "support": { + "anyOf": [ + { + "enum": [ + "supported", + "partly_supported", + "contradicted", + "not_in_source", + "unchecked" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Support" }, - "partial": { - "type": "boolean", - "description": "True when only some of the claims were accepted (the balance covered the first ones)." - } - }, - "required": [ - "batch_id", - "items" - ] - }, - "ReviewIssuesOutLegacy": { - "properties": { - "review_id": { - "title": "Review Id", - "type": "string" + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The passage from the source the support answer rests on, checked against the page text.", + "title": "Snippet" }, - "status": { - "description": "verifying: deep checks or citation checks are running.", - "enum": [ - "queued", - "assessing", - "verifying", - "completed", - "failed" + "rationale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ], - "title": "Status", - "type": "string" + "description": "The reviewer's sentence: reasoning, not a checked source.", + "title": "Rationale" }, - "outcome": { + "quote": { "anyOf": [ { "enum": [ - "clean", - "issues_found", - "incomplete", + "matched", + "not_in_source", "unchecked" ], "type": "string" @@ -11828,14 +12286,9 @@ "type": "null" } ], - "description": "Null until terminal. clean covers the selected claims at the policy's chosen depth. issues_found with an empty issues[] means the rows are in citation_issues[].", - "title": "Outcome" - }, - "created_at": { - "title": "Created At", - "type": "string" + "title": "Quote" }, - "completed_at": { + "missing_quote": { "anyOf": [ { "type": "string" @@ -11844,107 +12297,76 @@ "type": "null" } ], - "title": "Completed At" - }, - "language": { - "title": "Language", - "type": "string" - }, - "policy": { - "$ref": "#/components/schemas/ReviewPolicyOut" - }, - "summary": { - "$ref": "#/components/schemas/ReviewSummaryOutLegacy" - }, - "credits": { - "$ref": "#/components/schemas/ReviewCreditsOut" + "description": "The first of quotes the source does not contain; null unless quote is not_in_source.", + "title": "Missing Quote" }, - "poll_after_seconds": { + "doi_registered": { "anyOf": [ { - "type": "integer" + "type": "boolean" }, { "type": "null" } ], - "title": "Poll After Seconds" - }, - "issues": { - "items": { - "$ref": "#/components/schemas/ReviewIssueOutLegacy" - }, - "title": "Issues", - "type": "array" - }, - "failures": { - "items": { - "$ref": "#/components/schemas/ReviewFailedRowOutLegacy" - }, - "title": "Failures", - "type": "array" + "title": "Doi Registered" }, - "citation_issues": { - "default": [], - "description": "Citations whose source does not say what the draft says, the most serious first; partly_supported rows are not issues and are only in citations[]. [] when the check was not asked.", - "items": { - "$ref": "#/components/schemas/ReviewCitationIssueOutLegacy" - }, - "title": "Citation Issues", - "type": "array" + "metadata": { + "anyOf": [ + { + "enum": [ + "consistent", + "mismatch", + "unchecked" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Metadata" }, - "citation_failures": { + "metadata_differences": { "default": [], "items": { - "$ref": "#/components/schemas/ReviewCitationFailedRowOutLegacy" + "$ref": "#/components/schemas/ReviewMetadataDifferenceOut" }, - "title": "Citation Failures", + "title": "Metadata Differences", "type": "array" }, - "more_claims": { + "registered": { "anyOf": [ { - "items": { - "type": "string" - }, - "type": "array" + "$ref": "#/components/schemas/ReviewRegisteredOut" }, { "type": "null" } - ], - "description": "Claims found past max_assessments, in the draft's order: found but not checked; send them in a later request to check them. Null until the draft is read.", - "title": "More Claims" + ] }, - "more_claim_locations": { + "unchecked_reason": { "anyOf": [ { - "items": { - "$ref": "#/components/schemas/ClaimLocationOut" - }, - "type": "array" + "type": "string" }, { "type": "null" } ], - "description": "Where the draft makes each of `more_claims`, in the same order: `{claim, positions}`, the shape `/extract` returns with `locate: true` (see a claim row's `positions`). Null until the draft is read.", - "title": "More Claim Locations" + "description": "Why the finding is unchecked; an extensible string.", + "title": "Unchecked Reason" }, - "more_citations": { + "hint": { "anyOf": [ { - "items": { - "$ref": "#/components/schemas/ReviewMoreCitationOut" - }, - "type": "array" + "type": "string" }, { "type": "null" } ], - "description": "Citations found past the ones selected (up to 100): found but not checked; send them in a later request to check them. Null until the draft is read.", - "title": "More Citations" + "title": "Hint" }, "failure": { "anyOf": [ @@ -11955,32 +12377,38 @@ "type": "null" } ] - }, - "view": { - "const": "issues", - "title": "View", - "type": "string" } }, "required": [ - "review_id", - "status", - "outcome", - "created_at", - "completed_at", - "language", - "policy", - "summary", - "credits", - "poll_after_seconds", - "issues", - "failures", - "failure", - "view" + "status" ], - "title": "ReviewIssuesOutLegacy", + "title": "ReviewCitationCheckOutLegacy", + "type": "object", + "description": "`ReviewCitationCheckOut` as `2026-05-13` sends it: the same fields, with the older shape of what it contains." + }, + "BatchAcceptedOutLegacy": { + "title": "BatchAcceptedOutLegacy", "type": "object", - "description": "`ReviewIssuesOut` as `2026-05-13` sends it: the same fields, with the older shape of what it contains." + "description": "`BatchAcceptedOut` as `2026-05-13` sends it: the same fields, with the older shape of what it contains.", + "properties": { + "batch_id": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BatchAcceptedItemOutLegacy" + } + }, + "partial": { + "type": "boolean", + "description": "True when only some of the claims were accepted (the balance covered the first ones)." + } + }, + "required": [ + "batch_id", + "items" + ] } }, "securitySchemes": { @@ -12000,7 +12428,7 @@ "2026-05-13" ] }, - "description": "Read, but every request is answered in `2026-05-13` for now." + "description": "The API version to answer in, as a date. A date before `2026-10-11` (or a value that is not a date) gets `2026-05-13`. See https://lenz.io/docs/api-versions." } }, "headers": { @@ -12070,10 +12498,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookPayloadLegacy" + "$ref": "#/components/schemas/WebhookPayload" }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/WebhookPayload" }, "2026-05-13": { @@ -12099,10 +12527,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookPayloadLegacy" + "$ref": "#/components/schemas/WebhookPayload" }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/WebhookPayload" }, "2026-05-13": { @@ -12128,10 +12556,39 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookPayloadLegacy" + "$ref": "#/components/schemas/WebhookPayload" + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/WebhookPayload" + }, + "2026-05-13": { + "$ref": "#/components/schemas/WebhookPayloadLegacy" + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Your endpoint should return 2xx within 5 seconds. Non-2xx triggers retries at 10s / 60s / 600s (3 retries after the initial delivery, 4 attempts total)." + } + } + } + }, + "verification.cancelled": { + "post": { + "summary": "verification.cancelled", + "description": "Fires when POST /verify/{task_id}/cancel stopped the run: nothing was saved or charged. A 2026-05-13 submission receives verification.failed with failure_class \"cancelled\" instead.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookPayload" }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/WebhookPayload" }, "2026-05-13": { @@ -12157,10 +12614,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookPayloadLegacy" + "$ref": "#/components/schemas/WebhookPayload" }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/WebhookPayload" }, "2026-05-13": { @@ -12186,10 +12643,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReviewWebhookPayloadLegacy" + "$ref": "#/components/schemas/ReviewWebhookPayload" }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ReviewWebhookPayload" }, "2026-05-13": { @@ -12215,10 +12672,39 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReviewWebhookPayloadLegacy" + "$ref": "#/components/schemas/ReviewWebhookPayload" + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/ReviewWebhookPayload" + }, + "2026-05-13": { + "$ref": "#/components/schemas/ReviewWebhookPayloadLegacy" + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Your endpoint should return 2xx within 5 seconds. Non-2xx triggers retries at 10s / 60s / 600s (3 retries after the initial delivery, 4 attempts total)." + } + } + } + }, + "review.cancelled": { + "post": { + "summary": "review.cancelled", + "description": "Fires once when POST /reviews/{review_id}/cancel stopped the review. What was not delivered was not charged. A 2026-05-13 submission receives review.failed instead.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewWebhookPayload" }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/ReviewWebhookPayload" }, "2026-05-13": { @@ -12244,10 +12730,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckWebhookPayloadLegacy" + "$ref": "#/components/schemas/CitecheckWebhookPayload" }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckWebhookPayload" }, "2026-05-13": { @@ -12273,10 +12759,39 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CitecheckWebhookPayloadLegacy" + "$ref": "#/components/schemas/CitecheckWebhookPayload" + }, + "x-lenz-versions": { + "2026-10-11": { + "$ref": "#/components/schemas/CitecheckWebhookPayload" + }, + "2026-05-13": { + "$ref": "#/components/schemas/CitecheckWebhookPayloadLegacy" + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Your endpoint should return 2xx within 5 seconds. Non-2xx triggers retries at 10s / 60s / 600s (3 retries after the initial delivery, 4 attempts total)." + } + } + } + }, + "citecheck.cancelled": { + "post": { + "summary": "citecheck.cancelled", + "description": "Fires once when POST /citechecks/{citecheck_id}/cancel stopped the check. Citations not checked were not charged. A 2026-05-13 submission receives citecheck.failed instead.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CitecheckWebhookPayload" }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/CitecheckWebhookPayload" }, "2026-05-13": { @@ -12302,10 +12817,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookPayloadLegacy" + "$ref": "#/components/schemas/WebhookPayload" }, "x-lenz-versions": { - "next": { + "2026-10-11": { "$ref": "#/components/schemas/WebhookPayload" }, "2026-05-13": { From 3b395ffb12310757bdad4f93dd67a3054b58089a Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 22:22:38 +0300 Subject: [PATCH 51/67] test: the cancelled durable status is {status, task_id}; drop the stale after_a_while copy Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- ...erify__status_cancelled_after_a_while.json | 20 ------ ...erify__status_cancelled_after_a_while.json | 61 ------------------- ...erify__status_cancelled_after_a_while.json | 17 ------ tests/fixtures/parity/pickles.json | 12 ++-- tests/test_read_both_shapes.py | 16 ++++- 5 files changed, 21 insertions(+), 105 deletions(-) delete mode 100644 tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json delete mode 100644 tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json delete mode 100644 tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json diff --git a/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json b/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json deleted file mode 100644 index 40ec53e..0000000 --- a/tests/fixtures/parity/canonical/verify__status_cancelled_after_a_while.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "status": 200, - "body": { - "status": "failed", - "task_id": "7cff18da2d979d6370e54a160be121ad", - "failure": { - "code": "cancelled", - "detail": "The check was cancelled.", - "hint": null, - "failure_class": "cancelled", - "retryable": false, - "docs_url": "https://lenz.io/docs/errors#cancelled" - } - }, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - } -} diff --git a/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json b/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json deleted file mode 100644 index 820dfc6..0000000 --- a/tests/fixtures/parity/expected/verify__status_cancelled_after_a_while.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "dump": { - "candidates": [], - "claims": [], - "docs_url": "https://lenz.io/docs/errors#cancelled", - "error": "Cancelled.", - "failure_class": "cancelled", - "failure_detail": "", - "failure_reason": "cancelled", - "hint": "", - "progress": { - "elapsed_seconds": null, - "index": null, - "poll_after_seconds": null, - "step": "", - "total": null - }, - "reason": "", - "result": null, - "retryable": false, - "similar_claims": [], - "status": "failed", - "task_id": "7cff18da2d979d6370e54a160be121ad" - }, - "dump_unset": { - "docs_url": "https://lenz.io/docs/errors#cancelled", - "error": "Cancelled.", - "failure_class": "cancelled", - "failure_reason": "cancelled", - "retryable": false, - "status": "failed", - "task_id": "7cff18da2d979d6370e54a160be121ad" - }, - "render": "failed — Cancelled.\n", - "render_json": "{\n \"status\": \"failed\",\n \"task_id\": \"7cff18da2d979d6370e54a160be121ad\",\n \"reason\": \"\",\n \"hint\": \"\",\n \"progress\": {\n \"step\": \"\",\n \"index\": null,\n \"total\": null,\n \"elapsed_seconds\": null,\n \"poll_after_seconds\": null\n },\n \"result\": null,\n \"claims\": [],\n \"candidates\": [],\n \"similar_claims\": [],\n \"error\": \"Cancelled.\",\n \"failure_reason\": \"cancelled\",\n \"failure_detail\": \"\",\n \"failure_class\": \"cancelled\",\n \"retryable\": false,\n \"docs_url\": \"https://lenz.io/docs/errors#cancelled\"\n}\n", - "repr": "TaskStatus(status='failed', task_id='7cff18da2d979d6370e54a160be121ad', reason='', hint='', progress=Progress(step='', index=None, total=None, elapsed_seconds=None, poll_after_seconds=None), result=None, claims=[], candidates=[], similar_claims=[], error='Cancelled.', failure_reason='cancelled', failure_detail='', failure_class='cancelled', retryable=False, docs_url='https://lenz.io/docs/errors#cancelled')", - "wait": { - "cause": "Cancelled.", - "code": "", - "doc_url": "https://lenz.io/docs/errors", - "failure_class": "cancelled", - "failure_reason": "cancelled", - "fix": "Retry with a different claim, or check status.error for the diagnostic.", - "friendly_text": "Pipeline failed: Cancelled.\n Fix: Retry with a different claim, or check status.error for the diagnostic.", - "hint": "", - "message": "Pipeline failed: Cancelled.", - "payload_json": { - "error": { - "code": "pipeline_failed", - "fix": "Retry with a different claim, or check status.error for the diagnostic.", - "message": "Pipeline failed: Cancelled.", - "status": 0 - } - }, - "request_id": "", - "retryable": false, - "status_code": 0, - "task_id": "t", - "type": "LenzPipelineError" - } -} diff --git a/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json b/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json deleted file mode 100644 index d175b8e..0000000 --- a/tests/fixtures/parity/legacy/verify__status_cancelled_after_a_while.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "status": 200, - "body": { - "status": "failed", - "task_id": "7cff18da2d979d6370e54a160be121ad", - "error": "Cancelled.", - "failure_reason": "cancelled", - "failure_class": "cancelled", - "retryable": false, - "docs_url": "https://lenz.io/docs/errors#cancelled" - }, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Cache-Control": "no-store", - "Vary": "origin" - } -} diff --git a/tests/fixtures/parity/pickles.json b/tests/fixtures/parity/pickles.json index d096ce5..9035ab2 100644 --- a/tests/fixtures/parity/pickles.json +++ b/tests/fixtures/parity/pickles.json @@ -23,11 +23,11 @@ "assess__list_compound_item.json": "gASV2wIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMDlByaW1hcnkgY2xhaW0ulIwIbGFuZ3VhZ2WUjAJlbpSMB3ZlcmRpY3SUjAxNb3N0bHkgRmFsc2WUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTowKZXJyb3JfY29kZZROjBBjYW5kaWRhdGVfY2xhaW1zlF2UjBFpZGVudGlmaWVkX2NsYWltc5RdlCiMDVNlY29uZCBjbGFpbS6UjAxUaGlyZCBjbGFpbS6UZYwEaGludJSMWkFzc2Vzc2VkIHRoZSBtYWluIGNsYWltIG9ubHkuIFNlbmQgaWRlbnRpZmllZF9jbGFpbXMgYXMgdGhlaXIgb3duIGl0ZW1zIHRvIGNoZWNrIHRoZSByZXN0LpR1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCFoGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgKKYGUfZQoaAV9lChoDowOQSBwbGFpbiBjbGFpbS6UaBCMAmVulGgSjAxNb3N0bHkgRmFsc2WUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoIU51aCN9lGglj5QoaA5oIWgYaBtoFmgQaBRoHWgSaBdoGmgZkGgnTnViZYwFZXJyb3KUTmgajACUaBtdlIwLbW9yZV9jbGFpbXOUXZR1aCN9lGglj5QoaAdoNmgzkGgnTnViLg==", "assess__list_mixed_rows.json": "gASVxAQAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMIldhdGVyIGJvaWxzIGF0IDEwMCBDIGF0IHNlYSBsZXZlbC6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTowKZXJyb3JfY29kZZROjBBjYW5kaWRhdGVfY2xhaW1zlF2UjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoCimBlH2UKGgFfZQoaA6MC2hlbGxvIHRoZXJllGgQjAJlbpRoEowFRXJyb3KUaBSMA2xvd5RoFk5oF05oGE5oGU5oGowIbm9fY2xhaW2UaBtdlGgdXZRoH4xqVGhlIGlucHV0IGlzIGEgZ3JlZXRpbmcuIFNlbmQgb25lIGZhY3R1YWwgY2xhaW0sIG9yIHJ1biB0aGUgdGV4dCB0aHJvdWdoIC9leHRyYWN0IHRvIGVudW1lcmF0ZSBpdHMgY2xhaW1zLpR1aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjA52ZW5kb3IgaXMgZG93bpRoEIwCZW6UaBKMBUVycm9ylGgUjANsb3eUaBZOaBdOaBhOaBlOaBqMFHVwc3RyZWFtX3VuYXZhaWxhYmxllGgbXZRoHV2UaB+MTkEgbW9kZWwgcHJvdmlkZXIgd2FzIHVuYXZhaWxhYmxlIGZvciB0aGlzIGl0ZW0uIFJldHJ5IGl0OyBub3RoaW5nIHdhcyBjaGFyZ2VkLpR1aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjAxjYW5ub3QgZnJhbWWUaBCMAmVulGgSjAVFcnJvcpRoFIwDbG93lGgWTmgXTmgYTmgZTmgajA5mcmFtaW5nX2ZhaWxlZJRoG12UaB1dlGgfjINUaGlzIGl0ZW0gY291bGQgbm90IGJlIHByb2Nlc3NlZDsgcmV0cnlpbmcgaXQgYXMtaXMgd2lsbCBub3QgaGVscC4gU2VuZCBpdCByZXBocmFzZWQgYXMgb25lIGZhY3R1YWwgc3RhdGVtZW50OyBub3RoaW5nIHdhcyBjaGFyZ2VkLpR1aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViZYwFZXJyb3KUTmgajACUaBtdlIwLbW9yZV9jbGFpbXOUXZR1aCB9lGgij5QoaAdoT2hMkGgkTnViLg==", "assess__memo_hit.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwgVGhlIERhbnViZSBmbG93cyB0aHJvdWdoIFZpZW5uYS6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllIwgVGhlIHJpdmVyIHJ1bnMgdGhyb3VnaCB0aGUgY2l0eS6UjAdkaXNzZW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmVycm9yX2NvZGWUTowQY2FuZGlkYXRlX2NsYWltc5RdlIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA5oIGgZaBxoFmgQaBRoHmgSaBdoG2gakIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViYYwFZXJyb3KUTmgbjACUaBxdlIwLbW9yZV9jbGFpbXOUXZR1aCF9lGgjj5QoaAdoKWgmkGglTnViLg==", - "assess__stored_replay_200.json": "gASV3QEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwZVGhlIEVhcnRoIG9yYml0cyB0aGUgU3VuLpSMCGxhbmd1YWdllIwCZW6UjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwQdmVyaWZpY2F0aW9uX3VybJROjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UdWggfZRoIo+UKGgHaChoJZBoJE51Yi4=", "assess__single_no_claim.json": "gASVAgEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UjAVlcnJvcpSMHE5vIHZlcmlmaWFibGUgY2xhaW0gZGV0ZWN0ZWSUjAplcnJvcl9jb2RllIwIbm9fY2xhaW2UjBBjYW5kaWRhdGVfY2xhaW1zlF2UjAttb3JlX2NsYWltc5RdlHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA1oD2gLaAdoCZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", "assess__single_one_claim.json": "gASViwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwsVGhlIHJlZ2lzdHJ5IHJlcG9ydGVkIDQsMjAwIGZpbGluZ3MgaW4gMjAyNC6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllIxJVGhlIHJlZ2lzdHJ5IGxpc3RzIDQsMjAwIGZpbGluZ3MgZm9yIDIwMjQgYW5kIGhhcyBub3QgcmV2aXNlZCB0aGUgZmlndXJlLpSMB2Rpc3NlbnSUjE5UaGUgcmVnaXN0cnkgZmlndXJlIGlzIHByb3Zpc2lvbmFsLCBzbyB0aGUgMjAyNCB0b3RhbCBjYW5ub3QgeWV0IGJlIGNvbmZpcm1lZC6UjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCFoGWgdaBZoEGgUaB9oEmgXaBxoG5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oHIwAlGgdXZSMC21vcmVfY2xhaW1zlF2UdWgifZRoJI+UKGgHaCpoJ5BoJk51Yi4=", "assess__single_text_over_wave_more_claims.json": "gASVbgwAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMHUNsYWltIG51bWJlciAwIGlzIGRvY3VtZW50ZWQulIwIbGFuZ3VhZ2WUjAJlbpSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjBB2ZXJpZmljYXRpb25fdXJslE6MCXJhdGlvbmFsZZROjAdkaXNzZW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmVycm9yX2NvZGWUTowQY2FuZGlkYXRlX2NsYWltc5RdlIwRaWRlbnRpZmllZF9jbGFpbXOUXZSMBGhpbnSUTnWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgMSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgMiBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgMyBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNCBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNiBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgNyBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgOCBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB1DbGFpbSBudW1iZXIgOSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTAgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDoweQ2xhaW0gbnVtYmVyIDExIGlzIGRvY3VtZW50ZWQulGgQjAJlbpRoEowEVHJ1ZZRoFIwEaGlnaJRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJoCimBlH2UKGgFfZQoaA6MHkNsYWltIG51bWJlciAxMiBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTMgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDoweQ2xhaW0gbnVtYmVyIDE0IGlzIGRvY3VtZW50ZWQulGgQjAJlbpRoEowEVHJ1ZZRoFIwEaGlnaJRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJoCimBlH2UKGgFfZQoaA6MHkNsYWltIG51bWJlciAxNSBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTYgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDoweQ2xhaW0gbnVtYmVyIDE3IGlzIGRvY3VtZW50ZWQulGgQjAJlbpRoEowEVHJ1ZZRoFIwEaGlnaJRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJoCimBlH2UKGgFfZQoaA6MHkNsYWltIG51bWJlciAxOCBpcyBkb2N1bWVudGVkLpRoEIwCZW6UaBKMBFRydWWUaBSMBGhpZ2iUaBZOaBdOaBhOaBlOaBpOaBtdlGgdXZRoH051aCB9lGgij5QoaA5oH2gYaBtoFmgQaBRoHWgSaBdoGmgZkGgkTnViaAopgZR9lChoBX2UKGgOjB5DbGFpbSBudW1iZXIgMTkgaXMgZG9jdW1lbnRlZC6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmWMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UKIweQ2xhaW0gbnVtYmVyIDIwIGlzIGRvY3VtZW50ZWQulIweQ2xhaW0gbnVtYmVyIDIxIGlzIGRvY3VtZW50ZWQulGV1aCB9lGgij5QoaAdo+Wj2kGgkTnViLg==", "assess__single_text_several_claims.json": "gASVBwMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMIldhdGVyIGJvaWxzIGF0IDEwMCBDIGF0IHNlYSBsZXZlbC6UjAhsYW5ndWFnZZSMAmVulIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllE6MB2Rpc3NlbnSUTowRc3VnZ2VzdGVkX3Jld3JpdGWUTowKZXJyb3JfY29kZZROjBBjYW5kaWRhdGVfY2xhaW1zlF2UjBFpZGVudGlmaWVkX2NsYWltc5RdlIwEaGludJROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWJoCimBlH2UKGgFfZQoaA6MG1RoZSBNb29uIGlzIG1hZGUgb2YgY2hlZXNlLpRoEIwCZW6UaBKMBUZhbHNllGgUjARoaWdolGgWTmgXTmgYTmgZTmgaTmgbXZRoHV2UaB9OdWggfZRoIo+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZBoJE51YmgKKYGUfZQoaAV9lChoDowhVGhlIFBhY2lmaWMgaXMgdGhlIGRlZXBlc3Qgb2NlYW4ulGgQjAJlbpRoEowLTW9zdGx5IFRydWWUaBSMBm1lZGl1bZRoFk5oF05oGE5oGU5oGk5oG12UaB1dlGgfTnVoIH2UaCKPlChoDmgfaBhoG2gWaBBoFGgdaBJoF2gaaBmQaCROdWJljAVlcnJvcpROaBqMAJRoG12UjAttb3JlX2NsYWltc5RdlHVoIH2UaCKPlChoB2g+aDuQaCROdWIu", + "assess__stored_replay_200.json": "gASV3QEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UaACMC0Fzc2Vzc0NsYWltlJOUKYGUfZQoaAV9lCiMBWNsYWltlIwZVGhlIEVhcnRoIG9yYml0cyB0aGUgU3VuLpSMCGxhbmd1YWdllIwCZW6UjAd2ZXJkaWN0lIwEVHJ1ZZSMCmNvbmZpZGVuY2WUjARoaWdolIwQdmVyaWZpY2F0aW9uX3VybJROjAlyYXRpb25hbGWUTowHZGlzc2VudJROjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaB9oGGgbaBZoEGgUaB1oEmgXaBpoGZCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBWVycm9ylE5oGowAlGgbXZSMC21vcmVfY2xhaW1zlF2UdWggfZRoIo+UKGgHaChoJZBoJE51Yi4=", "assess__suggest_rewrite.json": "gASVBwMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwOQXNzZXNzUmVzcG9uc2WUk5QpgZR9lCiMCF9fZGljdF9flH2UKIwGY2xhaW1zlF2UKGgAjAtBc3Nlc3NDbGFpbZSTlCmBlH2UKGgFfZQojAVjbGFpbZSMJ1ZlbnVzIGlzIHRoZSBjbG9zZXN0IHBsYW5ldCB0byB0aGUgU3VuLpSMCGxhbmd1YWdllIwCZW6UjAd2ZXJkaWN0lIwFRmFsc2WUjApjb25maWRlbmNllIwEaGlnaJSMEHZlcmlmaWNhdGlvbl91cmyUTowJcmF0aW9uYWxllIw1TWVyY3VyeSwgbm90IFZlbnVzLCBpcyB0aGUgY2xvc2VzdCBwbGFuZXQgdG8gdGhlIFN1bi6UjAdkaXNzZW50lE6MEXN1Z2dlc3RlZF9yZXdyaXRllIwpTWVyY3VyeSBpcyB0aGUgY2xvc2VzdCBwbGFuZXQgdG8gdGhlIFN1bi6UjAplcnJvcl9jb2RllE6MEGNhbmRpZGF0ZV9jbGFpbXOUXZSMEWlkZW50aWZpZWRfY2xhaW1zlF2UjARoaW50lE51jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgOaCFoGWgdaBZoEGgUaB9oEmgXaBxoGpCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgKKYGUfZQoaAV9lChoDowpTWVyY3VyeSBpcyB0aGUgY2xvc2VzdCBwbGFuZXQgdG8gdGhlIFN1bi6UaBCMAmVulGgSjARUcnVllGgUjARoaWdolGgWTmgXjCBNZXJjdXJ5IGlzIHRoZSBpbm5lcm1vc3QgcGxhbmV0LpRoGU5oGk5oHE5oHV2UaB9dlGghTnVoIn2UaCSPlChoDmghaBloHWgWaBBoFGgfaBJoF2gcaBqQaCZOdWJljAVlcnJvcpROaByMAJRoHV2UjAttb3JlX2NsYWltc5RdlHVoIn2UaCSPlChoB2g2aDOQaCZOdWIu", "citecheck__get_checking.json": "gASV7AYAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwIY2hlY2tpbmeUjAdvdXRjb21llE6MCmNyZWF0ZWRfYXSUjBsyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NlqUjAxjb21wbGV0ZWRfYXSUTowScG9sbF9hZnRlcl9zZWNvbmRzlEsKjAZwb2xpY3mUaACMD0NpdGVjaGVja1BvbGljeZSTlCmBlH2UKGgFfZSMDW1heF9jaXRhdGlvbnOUSwJzjBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgWkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjBBDaXRlY2hlY2tTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NpdGF0aW9uc19mb3VuZJRLAowSY2l0YXRpb25zX3NlbGVjdGVklEsCjA5jaXRhdGlvbl9saW1pdJRLAowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJSJjA9jaXRhdGlvbl9jaGVja3OUaACMGVJldmlld0NpdGF0aW9uQ2hlY2tDb3VudHOUk5QpgZR9lChoBX2UKIwHY2hlY2tlZJRLAIwJdW5jaGVja2VklEsAjAZmYWlsZWSUSwB1aBd9lGgZj5QoaCxoLWgukGgbTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwB1aBd9lGgZj5QoaCNoJWgxaCRoJmgikGgbTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAHNoF32UaBmPlChoOpBoG051YowJY2l0YXRpb25zlF2UKGgAjA5SZXZpZXdDaXRhdGlvbpSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwJcmVmZXJlbmNllIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIwiVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LpSMBnF1b3Rlc5RdlIwIcG9zaXRpb26UTowGcmVzdWx0lE6MBWNoZWNrlGgAjBNSZXZpZXdDaXRhdGlvbkNoZWNrlJOUKYGUfZQoaAV9lChoCYwHcGVuZGluZ5SMCXBhZ2VfcmVhZJROjApwYWdlX3RpdGxllE6ME3BhZ2VfcHVibGlzaGVkX2RhdGWUTowNcGFnZV9sYW5ndWFnZZROjApzb3VyY2VfdXJslE6MDnNvdXJjZV92ZXJzaW9ulE6MB3N1cHBvcnSUTowHc25pcHBldJROjAlyYXRpb25hbGWUTowFcXVvdGWUTowNbWlzc2luZ19xdW90ZZROjA5kb2lfcmVnaXN0ZXJlZJROjAhtZXRhZGF0YZROjBRtZXRhZGF0YV9kaWZmZXJlbmNlc5RdlIwKcmVnaXN0ZXJlZJROjBB1bmNoZWNrZWRfcmVhc29ulE6MBGhpbnSUTowHZmFpbHVyZZROdWgXfZRoGY+UKGhZaGdoX2haaFtoaGhiaGNoCWhhaGRoYGhYaGZoXWhXaFxoXmhpkGgbTnVidWgXfZRoGY+UKGhKaE9oRWhQaERoSWhMaEdoTpBoG051YmhAKYGUfZQoaAV9lChoREsBaEWMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhHjB1odHRwczovL2V4YW1wbGUub3JnL3N0YXRlbWVudJRoSU5oSowiVGhlIGFnZW5jeSBzYWlkIHNvIGluIGEgc3RhdGVtZW50LpRoTF2UaE5OaE9OaFBoUimBlH2UKGgFfZQoaAmMB3BlbmRpbmeUaFdOaFhOaFlOaFpOaFtOaFxOaF1OaF5OaF9OaGBOaGFOaGJOaGNOaGRdlGhmTmhnTmhoTmhpTnVoF32UaBmPlChoWWhnaF9oWmhbaGhoYmhjaAloYWhkaGBoWGhmaF1oV2hcaF5oaZBoG051YnVoF32UaBmPlChoSmhPaEVoUGhEaEloTGhHaE6QaBtOdWJlaDFdlIwRY2l0YXRpb25fZmFpbHVyZXOUXZSMDm1vcmVfY2l0YXRpb25zlF2UaGlOdWgXfZSMCGxhbmd1YWdllIwCZW6Uc2gZj5QoaAtoNGgOaAxohGgJaBBoPWgxaH9oD2gHaBxogWhpkGgbTnViLg==", "citecheck__get_completed_clean.json": "gASV0QgAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwJQ2l0ZWNoZWNrlJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMDGNpdGVjaGVja19pZJSMCDhlYjQ5MzAylIwGc3RhdHVzlIwJY29tcGxldGVklIwHb3V0Y29tZZSMBWNsZWFulIwKY3JlYXRlZF9hdJSMGzIwMjYtMTAtMDFUMDk6MDc6MDAuMTIzNDU2WpSMDGNvbXBsZXRlZF9hdJSMGzIwMjYtMTAtMDFUMTA6MDc6MDAuMTIzNDU2WpSMEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROjAZwb2xpY3mUaACMD0NpdGVjaGVja1BvbGljeZSTlCmBlH2UKGgFfZSMDW1heF9jaXRhdGlvbnOUSwJzjBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAdzdW1tYXJ5lGgAjBBDaXRlY2hlY2tTdW1tYXJ5lJOUKYGUfZQoaAV9lCiMD2NpdGF0aW9uc19mb3VuZJRLAowSY2l0YXRpb25zX3NlbGVjdGVklEsCjA5jaXRhdGlvbl9saW1pdJRLAowWY2l0YXRpb25fbGltaXRfcmVhY2hlZJSJjA9jaXRhdGlvbl9jaGVja3OUaACMGVJldmlld0NpdGF0aW9uQ2hlY2tDb3VudHOUk5QpgZR9lChoBX2UKIwHY2hlY2tlZJRLAowJdW5jaGVja2VklEsAjAZmYWlsZWSUSwB1aBl9lGgbj5QoaC5oL2gwkGgdTnVijA9jaXRhdGlvbl9pc3N1ZXOUSwB1aBl9lGgbj5QoaCVoJ2gzaCZoKGgkkGgdTnVijAdjcmVkaXRzlGgAjA1SZXZpZXdDcmVkaXRzlJOUKYGUfZQoaAV9lIwHY2hhcmdlZJRLAnNoGX2UaBuPlChoPJBoHU51YowJY2l0YXRpb25zlF2UKGgAjA5SZXZpZXdDaXRhdGlvbpSTlCmBlH2UKGgFfZQojAVpbmRleJRLAIwJcmVmZXJlbmNllIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMCWNpdGVkX3VybJSMH2h0dHBzOi8vZXhhbXBsZS5nb3YvcmVwb3J0LTIwMjSUjANkb2mUTowJc3RhdGVtZW50lIwiVW5lbXBsb3ltZW50IGZlbGwgdG8gNC4xJSBpbiAyMDI0LpSMBnF1b3Rlc5RdlIwIcG9zaXRpb26UTowGcmVzdWx0lGgAjBRSZXZpZXdDaXRhdGlvblJlc3VsdJSTlCmBlH2UKGgFfZQojAdmaW5kaW5nlIwJc3VwcG9ydGVklIwGc291cmNllIwHc3VwcG9ydJSMCGlzX2lzc3VllIl1aBl9lGgbj5QoaFdoW2hZkGgdTnVijAVjaGVja5RoAIwTUmV2aWV3Q2l0YXRpb25DaGVja5STlCmBlH2UKGgFfZQoaAmMCWNvbXBsZXRlZJSMCXBhZ2VfcmVhZJSMBGZ1bGyUjApwYWdlX3RpdGxllIwnUGFnZSBhdCBodHRwczovL2V4YW1wbGUuZ292L3JlcG9ydC0yMDI0lIwTcGFnZV9wdWJsaXNoZWRfZGF0ZZROjA1wYWdlX2xhbmd1YWdllIwCZW6UjApzb3VyY2VfdXJslIwfaHR0cHM6Ly9leGFtcGxlLmdvdi9yZXBvcnQtMjAyNJSMDnNvdXJjZV92ZXJzaW9ulE6MB3N1cHBvcnSUjAlzdXBwb3J0ZWSUjAdzbmlwcGV0lIwTVGhlIHNvdXJjZSBzYXlzIHNvLpSMCXJhdGlvbmFsZZSME1RoZSBwYWdlIHN0YXRlcyBpdC6UjAVxdW90ZZROjA1taXNzaW5nX3F1b3RllE6MDmRvaV9yZWdpc3RlcmVklE6MCG1ldGFkYXRhlE6MFG1ldGFkYXRhX2RpZmZlcmVuY2VzlF2UjApyZWdpc3RlcmVklE6MEHVuY2hlY2tlZF9yZWFzb26UTowEaGludJROjAdmYWlsdXJllE51aBl9lGgbj5QoaGlofGhzaGpobGh9aHdoeGgJaHZoeWh1aGdoe2hvaGVobmhxaH6QaB1OdWJ1aBl9lGgbj5QoaExoUWhHaF5oRmhLaE5oSWhQkGgdTnViaEIpgZR9lChoBX2UKGhGSwFoR4wdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaEmMHWh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhLTmhMjCJUaGUgYWdlbmN5IHNhaWQgc28gaW4gYSBzdGF0ZW1lbnQulGhOXZRoUE5oUWhTKYGUfZQoaAV9lChoV4wJc3VwcG9ydGVklGhZjAdzdXBwb3J0lGhbiXVoGX2UaBuPlChoV2hbaFmQaB1OdWJoXmhgKYGUfZQoaAV9lChoCYwJY29tcGxldGVklGhljARmdWxslGhnjCVQYWdlIGF0IGh0dHBzOi8vZXhhbXBsZS5vcmcvc3RhdGVtZW50lGhpTmhqjAJlbpRobIwdaHR0cHM6Ly9leGFtcGxlLm9yZy9zdGF0ZW1lbnSUaG5OaG+MCXN1cHBvcnRlZJRocYwTVGhlIHNvdXJjZSBzYXlzIHNvLpRoc4wTVGhlIHBhZ2Ugc3RhdGVzIGl0LpRodU5odk5od05oeE5oeV2UaHtOaHxOaH1OaH5OdWgZfZRoG4+UKGhpaHxoc2hqaGxofWh3aHhoCWh2aHlodWhnaHtob2hlaG5ocWh+kGgdTnVidWgZfZRoG4+UKGhMaFFoR2heaEZoS2hOaEloUJBoHU51YmVoM12UjBFjaXRhdGlvbl9mYWlsdXJlc5RdlIwObW9yZV9jaXRhdGlvbnOUXZRofk51aBl9lIwIbGFuZ3VhZ2WUjAJlbpRzaBuPlChoC2g2aA9oDWinaAloEmg/aDNoomgRaAdoHmikaH6QaB1OdWIu", @@ -114,9 +114,8 @@ "verify__list_clamped_200.json": "gASVvgAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlIwFdG90YWyUSwCMBHBhZ2WUSwGMCXBhZ2Vfc2l6ZZRLZHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaApoC2gJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", "verify__list_empty_200.json": "gASVvgAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlIwFdG90YWyUSwCMBHBhZ2WUSwGMCXBhZ2Vfc2l6ZZRLFHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaApoC2gJaAeQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWIu", "verify__list_page2_200.json": "gASVbgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwQVmVyaWZpY2F0aW9uTGlzdJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAVpdGVtc5RdlGgAjBRWZXJpZmljYXRpb25MaXN0SXRlbZSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlIwHdmVyZGljdJSMBFRydWWUjApjb25maWRlbmNllIwEaGlnaJSMCmxlbnpfc2NvcmWUSwmMC2tleV9maW5kaW5nlIwuVGhlIEVhcnRoIGlzIGFwcHJveGltYXRlbHkgc3BoZXJpY2FsIGluIHNoYXBlLpSMEWV4ZWN1dGl2ZV9zdW1tYXJ5lIwWVGhlIGNsYWltIGlzIHZlcmlmaWVkLpSMEXN1Z2dlc3RlZF9yZXdyaXRllE6MCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoEGgOaBRoGGggaBJoFmgjaCJoHWgaaBtoH5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmGMBXRvdGFslEsDjARwYWdllEsCjAlwYWdlX3NpemWUSwJ1aCV9lGgnj5QoaCtoLGgqaAeQaClOdWIu", - "verify__stored_replay_202.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", "verify__select_202.json": "gASVpAEAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwNQmF0Y2hBY2NlcHRlZJSTlCmBlH2UKIwIX19kaWN0X1+UfZQojAhiYXRjaF9pZJSMEDdlYTljZGJkNDA2NGUzY2aUjAVpdGVtc5RdlChoAIwMVGFza0FjY2VwdGVklJOUKYGUfZQoaAV9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjBNUaGUgRWFydGggaXMgcm91bmQulHWMEl9fcHlkYW50aWNfZXh0cmFfX5R9lIwXX19weWRhbnRpY19maWVsZHNfc2V0X1+Uj5QoaBJoEJCMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51YmgMKYGUfZQoaAV9lChoEIwgMGI5MmI0NDNlYjY5YjljNzE0MzRmZGM2NGYyY2E4YmWUaBKMIVdhdGVyIGJvaWxzIGF0IDEwMEMgYXQgc2VhIGxldmVsLpR1aBR9lGgWj5QoaBJoEJBoGE51YmV1aBR9lGgWj5QoaAdoCZBoGE51Yi4=", - "verify__status_cancelled_after_a_while.json": "gASVNgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjApDYW5jZWxsZWQulIwOZmFpbHVyZV9yZWFzb26UjAljYW5jZWxsZWSUjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMCWNhbmNlbGxlZJSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMJWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNjYW5jZWxsZWSUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", + "verify__status_cancelled_durable.json": "gASVNgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjApDYW5jZWxsZWQulIwOZmFpbHVyZV9yZWFzb26UjAljYW5jZWxsZWSUjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMCWNhbmNlbGxlZJSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMJWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNjYW5jZWxsZWSUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", "verify__status_completed.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", "verify__status_completed_after_a_while.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", "verify__status_completed_after_a_while_modified_at_crosses_midnight_by_minutes.json": "gASVCQoAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDIzOjU1OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lIwgMjAyNi0xMC0wMlQwMDowMzowMC4xMjM0NTYrMDA6MDCUjAhsYW5ndWFnZZSMAmVulIwIY292ZXJhZ2WUaACMCENvdmVyYWdllJOUKYGUfZQoaAV9lChoB4wJdW5jb3ZlcmVklIwHcmVhc29uc5RdlIwEcGxhbpRhjA5jZXJ0aWZpY2F0ZV9pZJROjA9jZXJ0aWZpY2F0ZV91cmyUTowFYXNfb2aUTowIY3VycmVuY3mUjANFVVKUjANjYXCUTRAnjAlhZ2dyZWdhdGWUSiChBwCMDXRlcm1zX3ZlcnNpb26UjAJ2MZR1aBl9lGgbj5QoaLpotWi5aLZoB2i3aLtotGixkGgdTnVidWgZfZRoG4+UKGgkaC5oPmimaENoKGgsaEFoJmgqaKRoPGg6aEhoRWioaKpoSWhmaECQaB1OdWKMBmNsYWltc5RdlIwKY2FuZGlkYXRlc5RdlIwOc2ltaWxhcl9jbGFpbXOUXZSMBWVycm9ylGgMjA5mYWlsdXJlX3JlYXNvbpRoDIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUaAyMCXJldHJ5YWJsZZROjAhkb2NzX3VybJRoDHVoGX2UaBuPlChoHmgHaAmQaB1OdWIu", @@ -130,13 +129,14 @@ "verify__status_needs_input.json": "gASVGAMAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAtuZWVkc19pbnB1dJSMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMBnJlYXNvbpSMC211bHRpX2NsYWltlIwEaGludJSMYFRoZSB0ZXh0IGhvbGRzIHNldmVyYWwgZGlzdGluY3QgY2xhaW1zLiBTZW5kIHRoZSBvbmVzIHRvIGNoZWNrIHRvIFBPU1QgL3ZlcmlmeS97dGFza19pZH0vc2VsZWN0LpSMCHByb2dyZXNzlGgAjAhQcm9ncmVzc5STlCmBlH2UKGgFfZQojARzdGVwlIwAlIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UKGgAjA5DYW5kaWRhdGVDbGFpbZSTlCmBlH2UKGgFfZQojAR0ZXh0lIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMBmRvbWFpbpSMB1NjaWVuY2WUdWgbfZRoHY+UKGgqaCiQaB9OdWJoJCmBlH2UKGgFfZQoaCiMIVdhdGVyIGJvaWxzIGF0IDEwMEMgYXQgc2VhIGxldmVsLpRoKowHU2NpZW5jZZR1aBt9lGgdj5QoaCpoKJBoH051YmWMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoFowOZmFpbHVyZV9yZWFzb26UaBaMDmZhaWx1cmVfZGV0YWlslGgWjA1mYWlsdXJlX2NsYXNzlGgWjAlyZXRyeWFibGWUTowIZG9jc191cmyUaBZ1aBt9lGgdj5QoaA1oC2gJaAdoIZBoH051Yi4=", "verify__status_not_a_claim.json": "gASV3wIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lIyPVGhlIGlucHV0IGlzIGEgZ3JlZXRpbmcsIG5vdCBhIHN0YXRlbWVudCB0aGF0IGNhbiBiZSBjaGVja2VkLiBTZW5kIG9uZSBmYWN0dWFsIGNsYWltLCBvciBydW4gdGhlIHRleHQgdGhyb3VnaCAvZXh0cmFjdCB0byBlbnVtZXJhdGUgaXRzIGNsYWltcy6UjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjBdOb3QgYSB2ZXJpZmlhYmxlIGNsYWltLpSMDmZhaWx1cmVfcmVhc29ulIwLbm90X2FfY2xhaW2UjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMDWludmFsaWRfaW5wdXSUjAlyZXRyeWFibGWUiYwIZG9jc191cmyUjClodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjaW52YWxpZC1pbnB1dJR1aBp9lGgcj5QoaA1oCWgHaChoLmgraC1oJpBoHk51Yi4=", "verify__status_not_a_claim_after_a_while.json": "gASVVAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjB5QaXBlbGluZSBzdG9wcGVkOiBub3RfYV9jbGFpbS6UjA5mYWlsdXJlX3JlYXNvbpSMC25vdF9hX2NsYWltlIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjA1pbnZhbGlkX2lucHV0lIwJcmV0cnlhYmxllImMCGRvY3NfdXJslIwpaHR0cHM6Ly9sZW56LmlvL2RvY3MvZXJyb3JzI2ludmFsaWQtaW5wdXSUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", + "verify__status_processing.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAhyZXNlYXJjaJSMBWluZGV4lEsCjAV0b3RhbJRLBYwPZWxhcHNlZF9zZWNvbmRzlEsBjBJwb2xsX2FmdGVyX3NlY29uZHOUSwV1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYaBdoFmgUaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBnJlc3VsdJROjAZjbGFpbXOUXZSMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoDIwOZmFpbHVyZV9yZWFzb26UaAyMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlGgMjAlyZXRyeWFibGWUTowIZG9jc191cmyUaAx1aBp9lGgcj5QoaA5oB2gJkGgeTnViLg==", + "verify__status_processing_after_a_while.json": "gASVCgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAxhZGp1ZGljYXRpb26UjAVpbmRleJRLBIwFdG90YWyUSwWMD2VsYXBzZWRfc2Vjb25kc5RLAYwScG9sbF9hZnRlcl9zZWNvbmRzlEsFdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgXaBZoFGgZkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgafZRoHI+UKGgOaAdoCZBoHk51Yi4=", + "verify__status_task_stuck.json": "gASVewIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjDhUaGUgdGFzayB3YXMgbmV2ZXIgY29tcGxldGVkIGFuZCBoYXMgYmVlbiBtYXJrZWQgZmFpbGVkLpSMDmZhaWx1cmVfcmVhc29ulIwKdGFza19zdHVja5SMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlIwUdXBzdHJlYW1fdW5hdmFpbGFibGWUjAlyZXRyeWFibGWUiIwIZG9jc191cmyUjDBodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjdXBzdHJlYW0tdW5hdmFpbGFibGWUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", "verify__stored_progress_completed.json": "gASV5wkAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAljb21wbGV0ZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUaACMDFZlcmlmaWNhdGlvbpSTlCmBlH2UKGgFfZQojA92ZXJpZmljYXRpb25faWSUjAg2OGNhNGFhZpSMBWNsYWltlIwTVGhlIEVhcnRoIGlzIHJvdW5kLpSMCnZpc2liaWxpdHmUjAdwcml2YXRllIwFZGVwdGiUjAhzdGFuZGFyZJSMBmRvbWFpbpSMB1NjaWVuY2WUjAhlbnRpdGllc5RdlGgAjAlFbnRpdHlSZWaUk5QpgZR9lChoBX2UKIwEbmFtZZSMBUVhcnRolIwDcWlklE51aBl9lGgbj5QoaDVoN5BoHU51YmGMD3ByZXN1bWVkX2ludGVudJSMHlZlcmlmeSBhIGJhc2ljIHNjaWVudGlmaWMgZmFjdJSMB3ZlcmRpY3SUjARUcnVllIwKY29uZmlkZW5jZZSMBGhpZ2iUjApsZW56X3Njb3JllEsJjAtrZXlfZmluZGluZ5SMLlRoZSBFYXJ0aCBpcyBhcHByb3hpbWF0ZWx5IHNwaGVyaWNhbCBpbiBzaGFwZS6UjBFleGVjdXRpdmVfc3VtbWFyeZSMFlRoZSBjbGFpbSBpcyB2ZXJpZmllZC6UjAh3YXJuaW5nc5RdlIwZUmVsaWVzIG9uIGxpbWl0ZWQgc291cmNlc5RhjBFzdWdnZXN0ZWRfcmV3cml0ZZROjAdzb3VyY2VzlF2UKGgAjAZTb3VyY2WUk5QpgZR9lChoBX2UKIwLc291cmNlX25hbWWUjAROQVNBlIwFdGl0bGWUjBZUaGUgc2hhcGUgb2YgdGhlIEVhcnRolIwDdXJslIwaaHR0cHM6Ly9uYXNhLmV4YW1wbGUvc2hhcGWUjAdzbmlwcGV0lIwrU2F0ZWxsaXRlIGltYWdlcnkgc2hvd3MgYW4gb2JsYXRlIHNwaGVyb2lkLpSMBGRhdGWUjAoyMDI1LTAxLTE1lHVoGX2UaBuPlChoUGhYaFRoUmhWkGgdTnViaEwpgZR9lChoBX2UKGhQjANFU0GUaFKMEEVhcnRoIGZyb20gc3BhY2WUaFSMGWh0dHBzOi8vZXNhLmV4YW1wbGUvZWFydGiUaFaMK09yYml0YWwgbWVhc3VyZW1lbnRzIGNvbmZpcm0gdGhlIGN1cnZhdHVyZS6UaFiMCjIwMjUtMDEtMTaUdWgZfZRoG4+UKGhQaFhoVGhSaFaQaB1OdWJljAVhdWRpdJRoAIwFQXVkaXSUk5QpgZR9lChoBX2UKIwUYWRqdWRpY2F0aW9uX3N1bW1hcnmUjBZCb3RoIHJldmlld2VycyBhZ3JlZWQulIwLYXNzZXNzbWVudHOUXZQoaACMCkFzc2Vzc21lbnSUk5QpgZR9lChoBX2UKIwNcGFuZWxpc3RfbmFtZZSMClJldmlld2VyIEGUjApmb2N1c19hcmVhlIwnQ2xhaW0gUHJlY2lzaW9uICYgUXVhbnRpdGF0aXZlIEFjY3VyYWN5lIwFc2NvcmWUR0AiAAAAAAAAjAlyZWFzb25pbmeUjB1QcmVjaXNpb24gYW5hbHlzaXMgcmVhc29uaW5nLpRoRV2UdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJocSmBlH2UKGgFfZQoaHWMClJldmlld2VyIEKUaHeMB1NvdXJjZXOUaHlHQCIAAAAAAABoeowXU291cmNlIGF1ZGl0IHJlYXNvbmluZy6UaEVdlIwNd2VhayBzb3VyY2UgQZRhdWgZfZRoG4+UKGhFaHdodWh5aHqQaB1OdWJljApkZWJhdGVfcHJvlGgAjApEZWJhdGVTaWRllJOUKYGUfZQoaAV9lCiMBHJvbGWUjAhBZHZvY2F0ZZSMCGFyZ3VtZW50lIwkU2F0ZWxsaXRlIGltYWdlcnkgYW5kIGdlb2Rlc3kgYWdyZWUulIwIcmVidXR0YWyUjCJGbGF0LUVhcnRoIG9iamVjdGlvbnMgZG8gbm90IGhvbGQulHVoGX2UaBuPlChokWiTaI+QaB1OdWKMCmRlYmF0ZV9jb26UaIspgZR9lChoBX2UKGiPjAdTa2VwdGljlGiRjCJUaGUgRWFydGggaXMgbm90IGEgcGVyZmVjdCBzcGhlcmUulGiTjCBPYmxhdGUsIGJ1dCByb3VuZCBmb3IgdGhlIGNsYWltLpR1aBl9lGgbj5QoaJFok2iPkGgdTnVijA9wYW5lbF9hZ3JlZW1lbnSUjAl1bmFuaW1vdXOUdWgZfZRoG4+UKGhuaJdobGigaImQaB1OdWKMCmNyZWF0ZWRfYXSUjCAyMDI2LTEwLTAxVDA5OjA3OjAwLjEyMzQ1NiswMDowMJSMC21vZGlmaWVkX2F0lE6MCGxhbmd1YWdllIwCZW6UjAhjb3ZlcmFnZZRoAIwIQ292ZXJhZ2WUk5QpgZR9lChoBX2UKGgHjAl1bmNvdmVyZWSUjAdyZWFzb25zlF2UjARwbGFulGGMDmNlcnRpZmljYXRlX2lklE6MD2NlcnRpZmljYXRlX3VybJROjAVhc19vZpROjAhjdXJyZW5jeZSMA0VVUpSMA2NhcJRNECeMCWFnZ3JlZ2F0ZZRKIKEHAIwNdGVybXNfdmVyc2lvbpSMAnYxlHVoGX2UaBuPlChouWi0aLhotWgHaLZoumizaLCQaB1OdWJ1aBl9lGgbj5QoaCRoLmg+aKZoQ2goaCxoQWgmaCpopGg8aDpoSGhFaKdoqWhJaGZoQJBoHU51YowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgZfZRoG4+UKGgeaAdoCZBoHU51Yi4=", "verify__stored_progress_failed_crashed.json": "gASVOwIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjBBQaXBlbGluZSBmYWlsZWQulIwOZmFpbHVyZV9yZWFzb26UjAp0YXNrX2Vycm9ylIwOZmFpbHVyZV9kZXRhaWyUaAyMDWZhaWx1cmVfY2xhc3OUjAhpbnRlcm5hbJSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMJGh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnRlcm5hbJR1aBl9lGgbj5QoaAloB2gnaC1oKmgsaCWQaB1OdWIu", "verify__stored_progress_failed_insufficient_evidence.json": "gASVbAIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjCNQaXBlbGluZSBzdG9wcGVkIGF0OiByZXNlYXJjaF9lbXB0eZSMDmZhaWx1cmVfcmVhc29ulIwOcmVzZWFyY2hfZW1wdHmUjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5SMFWluc3VmZmljaWVudF9ldmlkZW5jZZSMCXJldHJ5YWJsZZSJjAhkb2NzX3VybJSMMWh0dHBzOi8vbGVuei5pby9kb2NzL2Vycm9ycyNpbnN1ZmZpY2llbnQtZXZpZGVuY2WUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", "verify__stored_progress_in_progress.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAhyZXNlYXJjaJSMBWluZGV4lEsCjAV0b3RhbJRLBYwPZWxhcHNlZF9zZWNvbmRzlEsBjBJwb2xsX2FmdGVyX3NlY29uZHOUSwV1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYaBdoFmgUaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBnJlc3VsdJROjAZjbGFpbXOUXZSMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoDIwOZmFpbHVyZV9yZWFzb26UaAyMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlGgMjAlyZXRyeWFibGWUTowIZG9jc191cmyUaAx1aBp9lGgcj5QoaA5oB2gJkGgeTnViLg==", - "verify__status_processing.json": "gASVBgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAhyZXNlYXJjaJSMBWluZGV4lEsCjAV0b3RhbJRLBYwPZWxhcHNlZF9zZWNvbmRzlEsBjBJwb2xsX2FmdGVyX3NlY29uZHOUSwV1jBJfX3B5ZGFudGljX2V4dHJhX1+UfZSMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgYaBdoFmgUaBmQjBRfX3B5ZGFudGljX3ByaXZhdGVfX5ROdWKMBnJlc3VsdJROjAZjbGFpbXOUXZSMCmNhbmRpZGF0ZXOUXZSMDnNpbWlsYXJfY2xhaW1zlF2UjAVlcnJvcpRoDIwOZmFpbHVyZV9yZWFzb26UaAyMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlGgMjAlyZXRyeWFibGWUTowIZG9jc191cmyUaAx1aBp9lGgcj5QoaA5oB2gJkGgeTnViLg==", - "verify__status_processing_after_a_while.json": "gASVCgIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjApwcm9jZXNzaW5nlIwHdGFza19pZJSMIDdjZmYxOGRhMmQ5NzlkNjM3MGU1NGExNjBiZTEyMWFklIwGcmVhc29ulIwAlIwEaGludJRoDIwIcHJvZ3Jlc3OUaACMCFByb2dyZXNzlJOUKYGUfZQoaAV9lCiMBHN0ZXCUjAxhZGp1ZGljYXRpb26UjAVpbmRleJRLBIwFdG90YWyUSwWMD2VsYXBzZWRfc2Vjb25kc5RLAYwScG9sbF9hZnRlcl9zZWNvbmRzlEsFdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlChoGGgXaBZoFGgZkIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUaAyMDmZhaWx1cmVfcmVhc29ulGgMjA5mYWlsdXJlX2RldGFpbJRoDIwNZmFpbHVyZV9jbGFzc5RoDIwJcmV0cnlhYmxllE6MCGRvY3NfdXJslGgMdWgafZRoHI+UKGgOaAdoCZBoHk51Yi4=", - "verify__status_task_stuck.json": "gASVewIAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwKVGFza1N0YXR1c5STlCmBlH2UKIwIX19kaWN0X1+UfZQojAZzdGF0dXOUjAZmYWlsZWSUjAd0YXNrX2lklIwgN2NmZjE4ZGEyZDk3OWQ2MzcwZTU0YTE2MGJlMTIxYWSUjAZyZWFzb26UjACUjARoaW50lGgMjAhwcm9ncmVzc5RoAIwIUHJvZ3Jlc3OUk5QpgZR9lChoBX2UKIwEc3RlcJRoDIwFaW5kZXiUTowFdG90YWyUTowPZWxhcHNlZF9zZWNvbmRzlE6MEnBvbGxfYWZ0ZXJfc2Vjb25kc5ROdYwSX19weWRhbnRpY19leHRyYV9flH2UjBdfX3B5ZGFudGljX2ZpZWxkc19zZXRfX5SPlIwUX19weWRhbnRpY19wcml2YXRlX1+UTnVijAZyZXN1bHSUTowGY2xhaW1zlF2UjApjYW5kaWRhdGVzlF2UjA5zaW1pbGFyX2NsYWltc5RdlIwFZXJyb3KUjDhUaGUgdGFzayB3YXMgbmV2ZXIgY29tcGxldGVkIGFuZCBoYXMgYmVlbiBtYXJrZWQgZmFpbGVkLpSMDmZhaWx1cmVfcmVhc29ulIwKdGFza19zdHVja5SMDmZhaWx1cmVfZGV0YWlslGgMjA1mYWlsdXJlX2NsYXNzlIwUdXBzdHJlYW1fdW5hdmFpbGFibGWUjAlyZXRyeWFibGWUiIwIZG9jc191cmyUjDBodHRwczovL2xlbnouaW8vZG9jcy9lcnJvcnMjdXBzdHJlYW0tdW5hdmFpbGFibGWUdWgZfZRoG4+UKGgJaAdoJ2gtaCpoLGglkGgdTnViLg==", + "verify__stored_replay_202.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", "verify__submit_202.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", "verify__submit_202_options.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", "verify__submit_202_text_alias.json": "gASV/AAAAAAAAACMDmxlbnpfaW8ubW9kZWxzlIwMVGFza0FjY2VwdGVklJOUKYGUfZQojAhfX2RpY3RfX5R9lCiMB3Rhc2tfaWSUjCA3Y2ZmMThkYTJkOTc5ZDYzNzBlNTRhMTYwYmUxMjFhZJSMCmNsYWltX3RleHSUjACUdYwSX19weWRhbnRpY19leHRyYV9flH2UKIwGc3RhdHVzlIwGcXVldWVklIwIY2hhaW5faWSUjBA5OWEwYTljYTg5ZTFlNzJjlHWMF19fcHlkYW50aWNfZmllbGRzX3NldF9flI+UKGgPaA1oB5CMFF9fcHlkYW50aWNfcHJpdmF0ZV9flE51Yi4=", diff --git a/tests/test_read_both_shapes.py b/tests/test_read_both_shapes.py index f2b8c7a..ac8b4df 100644 --- a/tests/test_read_both_shapes.py +++ b/tests/test_read_both_shapes.py @@ -141,7 +141,6 @@ def test_receipt_claim(): "verify__status_failed.json", "verify__status_failed_retryable.json", "verify__status_not_a_claim.json", - "verify__status_cancelled_after_a_while.json", ], ) def test_failed_status_failure_block(name): @@ -159,6 +158,21 @@ def test_failed_status_failure_block(name): assert canonical.error == legacy_body["error"] +@pytest.mark.parametrize("name", ["verify__status_cancelled_live.json", "verify__status_cancelled_durable.json"]) +def test_cancelled_status_has_no_failure_block(name): + """A task cancelled elsewhere is its own status with no failure block; the + 2.x fields still read what the original shape said of it.""" + legacy_body, body = _both(name) + assert "failure" not in body + canonical = TaskStatus.model_validate(body) + assert canonical.status == "cancelled" + assert canonical.failure is None + assert canonical.failure_reason == legacy_body["failure_reason"] == "cancelled" + assert canonical.failure_class == legacy_body["failure_class"] == "cancelled" + assert canonical.retryable is False + assert canonical.docs_url == legacy_body["docs_url"] + + def test_needs_input_options(): legacy_body, body = _both("verify__status_needs_input.json") canonical = TaskStatus.model_validate(body) From ae524d62c4dbe288ebb9c98e09d954ff6aa335a3 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 22:23:15 +0300 Subject: [PATCH 52/67] fix: an id is one path segment; a cancel keeps the server's code and refuses a wrong answer Every id that goes into a URL path is percent-encoded whole by one helper, so ../, ?, # and / in an id cannot leave the intended path; an empty id, . and .. raise ValueError before any request. The cancel calls keep the server's error code (not_found, use_review_cancel) and raise LenzAPIError for a 200 that is not the task, review or check asked for. The smoke test cancels twice and waits 30s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- src/lenz_io/client.py | 119 +++++++++++++++++++-------- src/lenz_io/errors.py | 5 ++ src/lenz_io/models.py | 12 +-- tests/test_cancel.py | 101 ++++++++++++++++++++++- tests/test_path_ids.py | 133 +++++++++++++++++++++++++++++++ tests/test_public_call_bodies.py | 6 +- tests/test_smoke_staging.py | 10 ++- 7 files changed, 336 insertions(+), 50 deletions(-) create mode 100644 tests/test_path_ids.py diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 2d5cf26..18ef0b2 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -86,6 +86,7 @@ from collections.abc import Callable, Iterator, Sequence from contextlib import contextmanager from typing import Any, Literal, TypedDict, TypeVar, overload +from urllib.parse import quote import httpx @@ -361,9 +362,10 @@ def get(self, verification_id: str) -> Verification: Raises :class:`LenzGoneError` (HTTP 410) when the account's retention period has removed the verification. """ + vid = _segment(verification_id, "verifications.get() needs a verification_id.") body = self._p._request( "GET", - f"/verifications/{verification_id}", + f"/verifications/{vid}", auth_required=False, auth_optional=True, # send the key if we have one → owner sees private rows ) @@ -387,13 +389,15 @@ def get_certificate(self, verification_id: str) -> Certificate: published open-source checker without involving Lenz. A withdrawn certificate is still served — it is the record of what was warranted. """ - body = self._p._request("GET", f"/verifications/{verification_id}/certificate") + vid = _segment(verification_id, "verifications.get_certificate() needs a verification_id.") + body = self._p._request("GET", f"/verifications/{vid}/certificate") return Certificate.model_validate(body) def delete(self, verification_id: str) -> bool: """Idempotent. Retry-on-404 returns True ("already deleted").""" + vid = _segment(verification_id, "verifications.delete() needs a verification_id.") try: - self._p._request("DELETE", f"/verifications/{verification_id}") + self._p._request("DELETE", f"/verifications/{vid}") return True except LenzError as exc: # Idempotent DELETE: if the row was already gone (e.g. previous @@ -417,9 +421,10 @@ def related(self, verification_id: str, *, limit: int = 5) -> RelatedVerificatio Raises :class:`LenzGoneError` (HTTP 410) when the account's retention period has removed the verification. """ + vid = _segment(verification_id, "verifications.related() needs a verification_id.") body = self._p._request( "GET", - f"/verifications/{verification_id}/related", + f"/verifications/{vid}/related", params={"limit": limit}, auth_required=False, auth_optional=True, # send the key if we have one → owner sees own rows @@ -442,7 +447,8 @@ def history(self, verification_id: str) -> AskHistory: Raises :class:`LenzGoneError` (HTTP 410) when the account's retention period has removed the verification. """ - body = self._p._request("GET", f"/ask/{verification_id}") + vid = _segment(verification_id, "ask.history() needs a verification_id.") + body = self._p._request("GET", f"/ask/{vid}") return AskHistory.model_validate(body) def send( @@ -485,6 +491,7 @@ def send( Raises :class:`LenzGoneError` (HTTP 410) when the account's retention period has removed the verification. """ + vid = _segment(verification_id, "ask.send() needs a verification_id.") payload: dict[str, Any] = {"message": message} if language: payload["language"] = language @@ -495,14 +502,15 @@ def send( with _carrying_key(key, unreadable=True): body = self._p._request( "POST", - f"/ask/{verification_id}", + f"/ask/{vid}", json=payload, headers=headers, ) return AskReply.model_validate(body) def reset(self, verification_id: str) -> bool: - self._p._request("DELETE", f"/ask/{verification_id}") + vid = _segment(verification_id, "ask.reset() needs a verification_id.") + self._p._request("DELETE", f"/ask/{vid}") return True @@ -622,6 +630,16 @@ def _first_page(page: int) -> int: return page +def _segment(value: Any, message: str) -> str: + """An id as ONE path segment: percent-encoded whole, so a ``/``, ``?``, ``#`` + or ``%`` in it cannot leave the intended path (httpx would otherwise cut the + URL there). ``""``, ``"."`` and ``".."`` raise ``ValueError(message)``: a + path normaliser eats the dots, and an empty id names the collection.""" + if not isinstance(value, str) or value in ("", ".", ".."): + raise ValueError(message) + return quote(value, safe="") + + def _call_key(idempotency_key: str | None, idempotency: bool) -> str | None: """The ``Idempotency-Key`` for one call: the caller's own, else a random one generated once per call (so every retry of that call carries it), else @@ -1054,10 +1072,13 @@ def cancel(self, task_id: str) -> CancelResult: run, so losing a race is not an error. Read the result: - ``cancelled`` is ``True`` and ``status`` is ``"cancelled"``: the run - is cancelled, by this call or an earlier one. - - ``cancelled`` is ``False``: it had already ended. ``status`` says - how: ``"completed"`` (the verification exists and was charged as - usual) or ``"failed"``. + is cancelled, by this call or an earlier one, so a repeated or + retried cancel answers ``True`` too. + - ``cancelled`` is ``False``: the run is not cancelled, and ``status`` + is its status, normally ``"completed"`` (the verification exists and + was charged as usual) or ``"failed"``. A task that ``select`` + already resolved answers ``False`` with ``"needs_input"``: cancel + the task ids ``select`` returned. ``client.wait(task_id)`` and ``get_status`` then see ``cancelled``; ``wait`` raises :class:`LenzPipelineError` with @@ -1073,9 +1094,12 @@ def cancel(self, task_id: str) -> CancelResult: Since 3.0. """ - if not task_id: - raise ValueError("cancel() needs a task_id.") - return CancelResult.model_validate(self._request("POST", f"/verify/{task_id}/cancel")) + tid = _segment(task_id, "cancel() needs a task_id.") + path = f"/verify/{tid}/cancel" + body = self._request("POST", path) + if not _is_cancel_body(body, task_id): + raise _unexpected_answer("POST", path) + return CancelResult.model_validate(body) # ── headline ergonomic ── @@ -1168,8 +1192,7 @@ def wait( every error until the timeout.) """ task_id = task if isinstance(task, str) else task.task_id - if not task_id: - raise ValueError("wait() requires a non-empty task_id (got an empty TaskAccepted.task_id).") + _segment(task_id, "wait() requires a non-empty task_id (got an empty TaskAccepted.task_id).") terminal, timed_out, stopped = self._poll_to_terminal([task_id], timeout, on_progress) if task_id in stopped: raise stopped[task_id] @@ -1404,14 +1427,13 @@ def get_review(self, review_id: str, *, view: str = "full") -> ReviewFull | Revi Raises :class:`LenzGoneError` (410) once the account's retention period has removed the review. """ - if not review_id: - raise ValueError("get_review() needs a review_id.") + rid = _segment(review_id, "get_review() needs a review_id.") if view == "issues": - body = self._request("GET", f"/reviews/{review_id}", params={"view": "issues"}) + body = self._request("GET", f"/reviews/{rid}", params={"view": "issues"}) return ReviewIssues.model_validate(body) if view != "full": raise ValueError(f"view must be 'full' or 'issues' (got {view!r}).") - return ReviewFull.model_validate(self._request("GET", f"/reviews/{review_id}")) + return ReviewFull.model_validate(self._request("GET", f"/reviews/{rid}")) def cancel_review(self, review_id: str) -> ReviewFull: """Stop a review (``POST /reviews/{review_id}/cancel``), including the @@ -1429,9 +1451,12 @@ def cancel_review(self, review_id: str) -> ReviewFull: Since 3.0. """ - if not review_id: - raise ValueError("cancel_review() needs a review_id.") - return ReviewFull.model_validate(self._request("POST", f"/reviews/{review_id}/cancel")) + rid = _segment(review_id, "cancel_review() needs a review_id.") + path = f"/reviews/{rid}/cancel" + body = self._request("POST", path) + if not _is_full_review_body(body, review_id): + raise _unexpected_answer("POST", path) + return ReviewFull.model_validate(body) def review_and_wait( self, @@ -1554,9 +1579,8 @@ def citecheck( def get_citecheck(self, citecheck_id: str) -> Citecheck: """Read a citation check. Raises :class:`LenzGoneError` (410) once the account's retention period has removed it.""" - if not citecheck_id: - raise ValueError("get_citecheck() needs a citecheck_id.") - return Citecheck.model_validate(self._request("GET", f"/citechecks/{citecheck_id}")) + cid = _segment(citecheck_id, "get_citecheck() needs a citecheck_id.") + return Citecheck.model_validate(self._request("GET", f"/citechecks/{cid}")) def cancel_citecheck(self, citecheck_id: str) -> Citecheck: """Stop a citation check (``POST /citechecks/{citecheck_id}/cancel``). @@ -1574,9 +1598,12 @@ def cancel_citecheck(self, citecheck_id: str) -> Citecheck: Since 3.0. """ - if not citecheck_id: - raise ValueError("cancel_citecheck() needs a citecheck_id.") - return Citecheck.model_validate(self._request("POST", f"/citechecks/{citecheck_id}/cancel")) + cid = _segment(citecheck_id, "cancel_citecheck() needs a citecheck_id.") + path = f"/citechecks/{cid}/cancel" + body = self._request("POST", path) + if not _is_citecheck_body(body, citecheck_id): + raise _unexpected_answer("POST", path) + return Citecheck.model_validate(body) def citecheck_and_wait( self, @@ -1636,7 +1663,7 @@ def timed_out(last: Citecheck | None) -> Exception: ) return self._wait_job( - f"/citechecks/{citecheck_id}", + f"/citechecks/{_segment(citecheck_id, '_wait_citecheck() needs a citecheck_id.')}", timeout=timeout, on_update=on_update, parse=lambda body: Citecheck.model_validate(body) if _is_citecheck_body(body, citecheck_id) else None, @@ -1664,7 +1691,7 @@ def timed_out(last: ReviewFull | None) -> Exception: ) return self._wait_job( - f"/reviews/{review_id}", + f"/reviews/{_segment(review_id, '_wait_review() needs a review_id.')}", timeout=timeout, on_update=on_update, parse=lambda body: ReviewFull.model_validate(body) if _is_full_review_body(body, review_id) else None, @@ -1816,7 +1843,7 @@ def _poll_to_terminal( try: body = self._request( "GET", - f"/verify/status/{task_id}", + f"/verify/status/{quote(task_id, safe='')}", max_retries=0, timeout=self._poll_timeout(remaining), ) @@ -2099,15 +2126,17 @@ def _assess( return AssessResponse.model_validate(body) def _select(self, task_id: str, *, texts: list[str], idempotency_key: str | None = None) -> BatchAccepted: + tid = _segment(task_id, "select() needs a task_id.") headers = {} if idempotency_key: headers["Idempotency-Key"] = idempotency_key with _carrying_key(idempotency_key, unreadable=True): - body = self._request("POST", f"/verify/{task_id}/select", json={"texts": texts}, headers=headers) + body = self._request("POST", f"/verify/{tid}/select", json={"texts": texts}, headers=headers) return BatchAccepted.model_validate(body) def _get_status(self, task_id: str) -> TaskStatus: - body = self._request("GET", f"/verify/status/{task_id}") + tid = _segment(task_id, "get_status() needs a task_id.") + body = self._request("GET", f"/verify/status/{tid}") return TaskStatus.model_validate(body) # ── HTTP plumbing ── @@ -2288,6 +2317,28 @@ def _send( raise LenzAPIError(message=f"{method} {path} failed without diagnostic") +def _unexpected_answer(method: str, path: str) -> LenzAPIError: + """A 200 whose body is not the thing asked for (a proxy page, another + task's body): an error, never a default-valued result.""" + return LenzAPIError( + message=f"{method} {path} returned an unexpected response body.", + cause="The answer is not the shape the API documents for this call.", + fix="Retry; if it persists, contact support (https://lenz.io/contact) with the request.", + doc_url="https://lenz.io/docs/errors", + ) + + +def _is_cancel_body(body: Any, task_id: str) -> bool: + """Whether a 200 is this task's cancel result: its id, a boolean + ``cancelled`` and a status.""" + return ( + isinstance(body, dict) + and body.get("task_id") == task_id + and isinstance(body.get("cancelled"), bool) + and isinstance(body.get("status"), str) + ) + + def _is_full_review_body(body: Any, review_id: str) -> bool: """Whether a 200 is this review's full view: its id, a status, and the three lists. A proxy page or another review's body is a failed poll, never diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 0b19ad8..88017f3 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -670,6 +670,10 @@ def _parse_body(raw: bytes | str | None) -> dict[str, Any]: #: /review and /citecheck (submit and read) kept their own error envelope. _REVIEW_FAMILY = re.compile(r"^/(?:review|reviews/[^/]+|citecheck|citechecks/[^/]+)$") +#: The cancel calls are new in 3.0: no earlier shape to read, so the server's +#: ``code`` stays on the error (``not_found``, ``use_review_cancel``, ...). +_CANCEL_PATH = re.compile(r"^/(?:verify|reviews|citechecks)/[^/]+/cancel$") + #: Codes the newer shape sends where the original error carried no ``code`` #: (outside /review and /citecheck, which always sent one). _CODELESS = frozenset( @@ -795,6 +799,7 @@ def _original_error(status: int, parsed: dict[str, Any], method: str, path: str) codeless = (code in _CODELESS and not (code == "too_many_items" and path == "/assess")) or ( code == "verification_not_ready" and path.startswith("/ask/") ) + codeless = codeless and not _CANCEL_PATH.match(path) if codeless: del out["code"] out.pop("errors", None) diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 5f90d07..4008775 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -888,11 +888,13 @@ class BatchAccepted(_Lax): class CancelResult(_Lax): """Returned by ``POST /verify/{task_id}/cancel`` (``Lenz.cancel``). - ``cancelled`` is ``True`` when the run is cancelled, by this call or an - earlier one; ``status`` is then ``"cancelled"``. It is ``False`` when the - run had already ended: ``status`` says how (``"completed"``, the - verification exists and was charged as usual, or ``"failed"``). The call - answers 200 either way. + ``cancelled`` is ``True`` whenever the run is cancelled, by this call or an + earlier one (so a repeated or retried cancel answers ``True``); ``status`` + is then ``"cancelled"``. It is ``False`` when the run is not cancelled: + ``status`` is its status, normally ``"completed"`` (the verification + exists and was charged as usual) or ``"failed"``; a task that ``select`` + already resolved answers ``False`` with ``"needs_input"``. The call answers + 200 either way. """ task_id: str = "" diff --git a/tests/test_cancel.py b/tests/test_cancel.py index 0e32bc2..90c1b92 100644 --- a/tests/test_cancel.py +++ b/tests/test_cancel.py @@ -107,8 +107,8 @@ def test_an_unknown_other_account_or_web_task_is_a_not_found( route = r.post(f"/verify/{TASK}/cancel").respond(404, json=_load(name)) with pytest.raises(LenzNotFoundError) as ei: client.cancel(TASK) - # As for every 404 the SDK maps: ``code`` is empty, the body keeps the server's. - assert (ei.value.status_code, ei.value.retryable, ei.value.body["code"]) == (404, False, "not_found") + assert (ei.value.status_code, ei.value.code, ei.value.retryable) == (404, "not_found", False) + assert ei.value.body["code"] == "not_found" assert route.call_count == 1 and slept == [] def test_a_reviews_deep_check_is_a_409_that_names_the_review_door(self, client: Lenz, slept: list[float]) -> None: @@ -251,7 +251,8 @@ def test_an_unknown_or_other_accounts_review_is_a_not_found(self, client: Lenz, route = r.post(f"/reviews/{REVIEW}/cancel").respond(404, json=_load("error_cancel_review_404.json")) with pytest.raises(LenzNotFoundError) as ei: client.cancel_review(REVIEW) - assert (ei.value.status_code, ei.value.body["code"]) == (404, "not_found") + assert (ei.value.status_code, ei.value.code) == (404, "not_found") + assert ei.value.body["code"] == "not_found" assert route.call_count == 1 and slept == [] def test_a_server_error_is_retried(self, client: Lenz, slept: list[float]) -> None: @@ -336,7 +337,8 @@ def test_an_unknown_check_or_a_review_id_is_a_not_found(self, client: Lenz, slep route = r.post(f"/citechecks/{CHECK}/cancel").respond(404, json=_load(name)) with pytest.raises(LenzNotFoundError) as ei: client.cancel_citecheck(CHECK) - assert (ei.value.status_code, ei.value.body["code"]) == (404, "not_found") + assert (ei.value.status_code, ei.value.code) == (404, "not_found") + assert ei.value.body["code"] == "not_found" assert route.call_count == 1 and slept == [] def test_a_server_error_is_retried(self, client: Lenz, slept: list[float]) -> None: @@ -365,3 +367,94 @@ def test_it_needs_a_key(self, unauth_client: Lenz) -> None: def test_the_cancel_result_is_lax_like_the_other_models() -> None: result = CancelResult.model_validate({}) assert (result.task_id, result.cancelled, result.status) == ("", False, "") + + +@pytest.mark.parametrize("path", ["/verify/t1/cancel", "/reviews/r1/cancel", "/citechecks/c1/cancel"]) +@pytest.mark.parametrize( + ("status", "code"), [(401, "not_authenticated"), (404, "not_found"), (422, "validation_error")] +) +def test_a_cancel_error_keeps_the_servers_code(path: str, status: int, code: str) -> None: + from lenz_io.errors import map_response_to_error + + err = map_response_to_error(status, json.dumps({"detail": "x", "code": code}).encode(), {}, endpoint=("POST", path)) + assert err.code == code + + +def test_another_endpoints_404_keeps_the_empty_code_2x_had() -> None: + from lenz_io.errors import map_response_to_error + + err = map_response_to_error( + 404, json.dumps({"detail": "x", "code": "not_found"}).encode(), {}, endpoint=("GET", "/verify/status/t1") + ) + assert err.code == "" + + +# ── an answer that is not the cancel's ── + + +@pytest.mark.parametrize( + "body", + [ + {}, + [], + "ok", + {"task_id": TASK}, + {"cancelled": True, "status": "cancelled"}, + {"task_id": "", "cancelled": True, "status": "cancelled"}, + {"task_id": "someone-else", "cancelled": True, "status": "cancelled"}, + {"task_id": TASK, "cancelled": "yes", "status": "cancelled"}, + {"task_id": TASK, "cancelled": True}, + ], +) +def test_a_cancel_answer_that_is_not_a_cancel_result_is_an_error(client: Lenz, body: Any) -> None: + from lenz_io import LenzAPIError + + with respx.mock(base_url=BASE) as r: + r.post(f"/verify/{TASK}/cancel").respond(200, json=body) + with pytest.raises(LenzAPIError) as ei: + client.cancel(TASK) + assert "unexpected" in ei.value.message.lower() + + +@pytest.mark.parametrize( + "body", + [ + {}, + {"review_id": REVIEW}, + {"status": "cancelled"}, + [], + {"review_id": "other", "status": "cancelled", "issues": [], "failures": [], "claims": []}, + ], +) +def test_a_review_cancel_answer_that_is_not_the_review_is_an_error(client: Lenz, body: Any) -> None: + from lenz_io import LenzAPIError + + with respx.mock(base_url=BASE) as r: + r.post(f"/reviews/{REVIEW}/cancel").respond(200, json=body) + with pytest.raises(LenzAPIError): + client.cancel_review(REVIEW) + + +@pytest.mark.parametrize( + "body", + [ + {}, + {"citecheck_id": CHECK}, + {"status": "cancelled"}, + [], + { + "citecheck_id": "other", + "status": "cancelled", + "citations": [], + "citation_issues": [], + "citation_failures": [], + }, + ], +) +def test_a_citecheck_cancel_answer_that_is_not_the_check_is_an_error(client: Lenz, body: Any) -> None: + from lenz_io import LenzAPIError + + with respx.mock(base_url=BASE) as r: + r.post(f"/citechecks/{CHECK}/cancel").respond(200, json=body) + with pytest.raises(LenzAPIError): + client.cancel_citecheck(CHECK) diff --git a/tests/test_path_ids.py b/tests/test_path_ids.py new file mode 100644 index 0000000..5a7fca7 --- /dev/null +++ b/tests/test_path_ids.py @@ -0,0 +1,133 @@ +"""An id goes into the URL path as ONE segment. + +httpx normalises dot segments and cuts a URL at ``?`` and ``#``, so an id put +into a path raw can send the request somewhere else: ``cancel_review`` with +``"../citechecks/c1"`` would POST ``/citechecks/c1/cancel``. Every id is +percent-encoded whole, ``"."`` and ``".."`` (which a path normaliser eats) are +refused like an empty id, and an ordinary id is on the wire byte for byte as +before. +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any +from urllib.parse import quote + +import httpx +import pytest +import respx + +from lenz_io import Lenz + +BASE = "https://lenz.io/api/v1" +HOSTILE = [ + "../citechecks/c1", + "a#x", + "batch?", + "a/b", + "a b", + "ünï/ç", + "%2e%2e", + "a%2Fb", + "x?y=1#z", + "\\..\\", + "a;b", +] + +#: (label, call(client, id), method, path template) +CALLS: list[tuple[str, Callable[[Lenz, str], Any], str, str]] = [ + ("get_status", lambda c, i: c.get_status(i), "GET", "/verify/status/{}"), + ("cancel", lambda c, i: c.cancel(i), "POST", "/verify/{}/cancel"), + ("select", lambda c, i: c.select(i, claims=["A claim."], idempotency=False), "POST", "/verify/{}/select"), + ("get_review", lambda c, i: c.get_review(i), "GET", "/reviews/{}"), + ("get_review issues", lambda c, i: c.get_review(i, view="issues"), "GET", "/reviews/{}"), + ("cancel_review", lambda c, i: c.cancel_review(i), "POST", "/reviews/{}/cancel"), + ("get_citecheck", lambda c, i: c.get_citecheck(i), "GET", "/citechecks/{}"), + ("cancel_citecheck", lambda c, i: c.cancel_citecheck(i), "POST", "/citechecks/{}/cancel"), + ("wait review", lambda c, i: c._wait_review(i, timeout=0), "GET", "/reviews/{}"), + ("wait citecheck", lambda c, i: c._wait_citecheck(i, timeout=0), "GET", "/citechecks/{}"), + ("wait", lambda c, i: c.wait(i, timeout=0), "GET", "/verify/status/{}"), + ("verifications.get", lambda c, i: c.verifications.get(i), "GET", "/verifications/{}"), + ( + "verifications.get_certificate", + lambda c, i: c.verifications.get_certificate(i), + "GET", + "/verifications/{}/certificate", + ), + ("verifications.delete", lambda c, i: c.verifications.delete(i), "DELETE", "/verifications/{}"), + ("verifications.related", lambda c, i: c.verifications.related(i), "GET", "/verifications/{}/related"), + ("ask.history", lambda c, i: c.ask.history(i), "GET", "/ask/{}"), + ("ask.send", lambda c, i: c.ask.send(i, message="Why?", idempotency=False), "POST", "/ask/{}"), + ("ask.reset", lambda c, i: c.ask.reset(i), "DELETE", "/ask/{}"), +] +IDS = [c[0] for c in CALLS] + + +def _send(call: Callable[[Lenz, str], Any], ident: str) -> list[httpx.Request]: + with Lenz(api_key="lenz_test_abc123", max_retries=0) as client, respx.mock(assert_all_called=False) as router: + router.route().respond(200, json={}) + try: + call(client, ident) + except Exception: + # The stub answers {}: whether the call can read it is not the point. + pass + return [c.request for c in router.calls] + + +@pytest.mark.parametrize("ident", HOSTILE) +@pytest.mark.parametrize(("label", "call", "method", "template"), CALLS, ids=IDS) +def test_a_hostile_id_stays_one_segment_of_the_intended_path( + label: str, call: Callable[[Lenz, str], Any], method: str, template: str, ident: str +) -> None: + requests = _send(call, ident) + assert requests, label + for request in requests: + assert request.method == method + assert request.url.raw_path.split(b"?")[0] == ("/api/v1" + template.format(quote(ident, safe=""))).encode() + assert request.url.fragment == "" + + +@pytest.mark.parametrize( + ("call", "path"), + [ + (lambda c: c.cancel_review("../citechecks/c1"), "/api/v1/reviews/..%2Fcitechecks%2Fc1/cancel"), + (lambda c: c.cancel("a#x"), "/api/v1/verify/a%23x/cancel"), + (lambda c: c.cancel("batch?"), "/api/v1/verify/batch%3F/cancel"), + (lambda c: c.cancel_citecheck("a/b"), "/api/v1/citechecks/a%2Fb/cancel"), + (lambda c: c.get_status("ünï"), "/api/v1/verify/status/%C3%BCn%C3%AF"), + ], +) +def test_the_traversal_examples(call: Callable[[Lenz], Any], path: str) -> None: + requests = _send(lambda c, _i: call(c), "") + assert [r.url.raw_path.decode() for r in requests] == [path] + + +@pytest.mark.parametrize("ident", ["", ".", ".."]) +@pytest.mark.parametrize(("label", "call", "method", "template"), CALLS, ids=IDS) +def test_an_empty_dot_or_dotdot_id_is_refused_before_any_request( + label: str, call: Callable[[Lenz, str], Any], method: str, template: str, ident: str +) -> None: + with Lenz(api_key="lenz_test_abc123", max_retries=0) as client, respx.mock(assert_all_called=False) as router: + router.route().respond(200, json={}) + with pytest.raises(ValueError): + call(client, ident) + assert router.calls.call_count == 0 + + +@pytest.mark.parametrize( + ("label", "call", "method", "template"), + [c for c in CALLS if c[0] not in ("wait review", "wait citecheck", "wait")], + ids=[i for i in IDS if i not in ("wait review", "wait citecheck", "wait")], +) +def test_an_ordinary_id_is_on_the_wire_unchanged( + label: str, call: Callable[[Lenz, str], Any], method: str, template: str +) -> None: + for ident in ("d6b2bd72", "3f2a9c1e5b7d4a608c1d2e3f4a5b6c7d", "tsk_abc-123.v2~x"): + requests = _send(call, ident) + assert requests[0].url.raw_path.split(b"?")[0] == ("/api/v1" + template.format(ident)).encode() + + +def test_a_dotted_id_that_is_not_all_dots_is_fine() -> None: + requests = _send(lambda c, i: c.get_review(i), "a..b") + assert requests[0].url.raw_path == b"/api/v1/reviews/a..b" diff --git a/tests/test_public_call_bodies.py b/tests/test_public_call_bodies.py index c174ef1..fbc09fb 100644 --- a/tests/test_public_call_bodies.py +++ b/tests/test_public_call_bodies.py @@ -402,9 +402,9 @@ class TestCancels: @pytest.mark.parametrize( ("method", "path", "fixture"), [ - ("cancel", "/verify/t1/cancel", "cancel_verify_cancelled.json"), - ("cancel_review", "/reviews/r1/cancel", "cancel_review_cancelled.json"), - ("cancel_citecheck", "/citechecks/c1/cancel", "cancel_citecheck_cancelled.json"), + ("cancel", "/verify/3f2a9c1e5b7d4a608c1d2e3f4a5b6c7d/cancel", "cancel_verify_cancelled.json"), + ("cancel_review", "/reviews/d6b2bd72/cancel", "cancel_review_cancelled.json"), + ("cancel_citecheck", "/citechecks/12bbbf65/cancel", "cancel_citecheck_cancelled.json"), ], ) def test_no_body_no_key(self, client: Lenz, method: str, path: str, fixture: str) -> None: diff --git a/tests/test_smoke_staging.py b/tests/test_smoke_staging.py index 86149c7..b866ce1 100644 --- a/tests/test_smoke_staging.py +++ b/tests/test_smoke_staging.py @@ -62,17 +62,19 @@ def test_a_run_can_be_cancelled(smoke_client): """``cancel`` answers 200 whatever the state of the run: stopped by this call, or already ended. Neither is an error, so the test cannot be flaky. - A cancelled run is not charged. A cache hit (an answer the API already - holds) has finished by the time we cancel, which is the other branch.""" + A cancelled verification is not charged. A cache hit (an answer the API + already holds) has finished by the time we cancel, which is the other + branch. Not the quickstart claim, so it is less likely to be one.""" started = smoke_client.verify(claim="The Eiffel Tower is in Paris", depth="low") result = smoke_client.cancel(started.task_id) assert result.task_id == started.task_id if result.cancelled: assert result.status == "cancelled" # Safe to repeat: still cancelled, and a wait ends on it at once. - assert smoke_client.cancel(started.task_id).cancelled is True + again = smoke_client.cancel(started.task_id) + assert (again.cancelled, again.status) == (True, "cancelled") with pytest.raises(LenzPipelineError) as raised: - smoke_client.wait(started.task_id, timeout=60) + smoke_client.wait(started.task_id, timeout=30) assert raised.value.failure_class == "cancelled" else: # It finished first; nothing was left to stop. From 6c72bde0204864843380261a9153561b52453083 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 22:23:44 +0300 Subject: [PATCH 53/67] docs: scope the cancel charge wording, say cancelled stays true on a repeat, note path-encoded ids Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 23 ++++++++++++++++++----- README.md | 9 ++++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d80dd8..57f4311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,13 @@ Deprecated). ### Changed +- **An id goes into the URL path as one segment.** Every method that puts an + id in a path (`get_status`, `select`, `get_review`, `get_citecheck`, the + cancels, `verifications.*`, `ask.*`, the waits) percent-encodes it whole, so + an id holding `/`, `?`, `#` or `..` can no longer send the request to another + path. An ordinary id is on the wire byte for byte as before. An empty id, `"."` + and `".."` raise `ValueError` before any request (the methods that took an + empty id now raise as `get_review` and `wait` already did). - **The SDK asks for the API's current response shape, and reads only that.** Every request sends `X-Lenz-API-Version: 2026-10-11` (`lenz_io.API_VERSION`; 2.x sent `2026-05-13`). In that shape each field, status and error code has one name @@ -186,16 +193,22 @@ Deprecated). review, its deep checks and its citation checks, and returns the full `ReviewFull` (what `get_review` returns); `client.cancel_citecheck(citecheck_id)` returns the `Citecheck`. All three answer 200 whatever the state of the run: - `cancelled=False` on a `cancel` means the run had already ended, and - `status` says how (`completed` or `failed`); a review or check that had - ended comes back unchanged. They send no body and no `Idempotency-Key` + `cancelled=True` whenever the run is cancelled, by this call or an earlier + one, so a repeated or retried cancel answers True; `cancelled=False` means + the run is not cancelled and `status` is its status, normally `completed` + or `failed`. A task that `select` already resolved answers `cancelled=False` + with `needs_input`: cancel the task ids `select` returned. A review or check + that had ended comes back unchanged. They send no body and no `Idempotency-Key` (cancelling twice is safe), and are retried on a 5xx or a dropped connection like any call that is safe to repeat. An unknown id, another account's, or (for `cancel`) a task started on the website raises `LenzNotFoundError`. A task that is a review's deep check raises a `LenzError` with `code == "use_review_cancel"` (409): cancel the review - instead; it is sent once, never waited on or resent. A cancelled run is not - charged; a `wait` on it raises the failed error with + instead; it is sent once, never waited on or resent. A cancelled + verification is not charged and saves nothing; a cancelled review or citation + check keeps charged what it delivered before the cancel (quick checks served, + deep checks that finished, citations checked) and the rest is refunded or + never charged. A `wait` on a cancelled run raises the failed error with `failure_class == "cancelled"`. Needs the API to serve version `2026-10-11`. - **`VerificationCancelled`** (new webhook event class, `verification.cancelled`), and `review.cancelled` / `citecheck.cancelled` typed as `ReviewEvent` / diff --git a/README.md b/README.md index fa29f33..023f231 100644 --- a/README.md +++ b/README.md @@ -971,9 +971,12 @@ print(check.status, check.credits.charged) - `cancel(task_id)` returns a `CancelResult`. `cancelled=True` with `status == "cancelled"` means the run is cancelled, by this call or an - earlier one. `cancelled=False` means it had already ended: `status` is - `"completed"` (the verification exists and was charged as usual) or - `"failed"`. A run waiting on `select` is cancelled too. + earlier one, so a repeated or retried cancel answers `True` too. + `cancelled=False` means it is not cancelled, and `status` is the run's + status, normally `"completed"` (the verification exists and was charged as + usual) or `"failed"`. A run waiting on `select` is cancelled too. A task that + `select` already resolved answers `cancelled=False` with `needs_input`: cancel + the task ids `select` returned. - `cancel_review(review_id)` stops the review and everything in it: its quick checks, its deep checks and its citation checks. It returns the review as it stands, `status == "cancelled"`; a review that had already ended is returned From 42e676d848379c5b2c1c891c4d02c4a4aac7ad64 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 22:51:37 +0300 Subject: [PATCH 54/67] docs: using Lenz from async code (worker threads, FastAPI, webhooks) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 023f231..68775dc 100644 --- a/README.md +++ b/README.md @@ -1159,6 +1159,54 @@ batch = client.verify_batch( ) ``` +## Using Lenz from async code + +The client is synchronous. Lenz calls take a while (`assess` about 15 s, a deep check about +90 s, a review a few minutes), so calling one directly inside an `async def` blocks the event +loop for that long: in a FastAPI or aiohttp server, every other request on that worker +waits. Run the call in a worker thread instead: + +```python +import asyncio +from lenz_io import Lenz + +client = Lenz() # one client for the whole app; it is safe to share across threads + +async def quick_check(claim: str) -> str: + out = await asyncio.to_thread(client.assess, claim=claim) + return out.claims[0].verdict +``` + +The same applies to every method, and above all to the ones that wait (`verify_and_wait`, +`verify_batch_and_wait`, `review_and_wait`, `citecheck_and_wait`, `wait`): never call them +directly inside an async handler. In FastAPI, a plain `def` route runs in the thread pool +already: + +```python +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from fastapi import FastAPI +from lenz_io import Lenz + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + app.state.lenz = Lenz() + yield + app.state.lenz.close() + +app = FastAPI(lifespan=lifespan) + +@app.post("/check") +def check(claim: str) -> dict[str, object]: # `def`, not `async def`: FastAPI runs it in a thread + v = app.state.lenz.verify_and_wait(claim) + return {"verdict": v.verdict, "score": v.lenz_score} +``` + +For a long check behind a web request, `verify(..., webhook_url=...)` returns at once and +Lenz posts the result to you when it is done (see [Webhooks](#webhooks)). That ties up no +thread for the wait. To check many claims at once, send them in one call +(`assess(claims=[...])`, `verify_batch`) rather than one thread per claim. + ## Configuration ```python From e637d1b64a41a2d0ed1cd1d19c57ab22cc5763b9 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 23:40:20 +0300 Subject: [PATCH 55/67] test: freeze what every public call puts on the wire Every public method and call form: URL with its query string, raw body bytes, ordered headers, the four httpx.Timeout components (incl. the extract/assess floors under seven client timeout configurations and a wait's capped polls), retry and poll sleeps under a fake clock, and how the call ends. Recorded from the current code, before request options. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- tests/fixtures/freeze/requests.json | 8839 +++++++++++++++++++++++++++ tests/freeze_cases.py | 350 ++ tests/freeze_harness.py | 160 + tests/test_request_freeze.py | 71 + 4 files changed, 9420 insertions(+) create mode 100644 tests/fixtures/freeze/requests.json create mode 100644 tests/freeze_cases.py create mode 100644 tests/freeze_harness.py create mode 100644 tests/test_request_freeze.py diff --git a/tests/fixtures/freeze/requests.json b/tests/fixtures/freeze/requests.json new file mode 100644 index 0000000..59303a0 --- /dev/null +++ b/tests/fixtures/freeze/requests.json @@ -0,0 +1,8839 @@ +{ + "ask_history": { + "outcome": { + "result": "AskHistory" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/ask/v1" + } + ], + "sleeps": [] + }, + "ask_reset": { + "outcome": { + "result": "bool" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "DELETE", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/ask/v1" + } + ], + "sleeps": [] + }, + "ask_send": { + "outcome": { + "result": "AskReply" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"message\":\"Why?\",\"language\":\"auto\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "36" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/ask/v1" + } + ], + "sleeps": [] + }, + "assess_claim": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 100.0, + "pool": 100.0, + "read": 100.0, + "write": 100.0 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "assess_claims": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"claims\":[\"A.\",\"B.\"],\"language\":\"auto\",\"suggest_rewrite\":true}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "63" + ] + ], + "method": "POST", + "timeout": { + "connect": 100.0, + "pool": 100.0, + "read": 100.0, + "write": 100.0 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "assess_timeout_20": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 20, + "pool": 20, + "read": 20, + "write": 20 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "borrowed_10:assess": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 100.0, + "pool": 100.0, + "read": 100.0, + "write": 100.0 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "borrowed_10:extract": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 150.0, + "pool": 150.0, + "read": 150.0, + "write": 150.0 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "borrowed_10:extract_timeout_9": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 9, + "pool": 9, + "read": 9, + "write": 9 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "borrowed_10:review_and_wait": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 10.0, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 10.0, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 10.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 2.0, + "pool": 2.0, + "read": 2.0, + "write": 2.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [ + 10.0 + ] + }, + "borrowed_10:usage": { + "outcome": { + "result": "Usage" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 10.0, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [] + }, + "borrowed_10:wait": { + "outcome": { + "code": "", + "error": "LenzTimeoutError", + "idempotency_key": null, + "message": "wait timed out after 12s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 10.0, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 10.0, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 6.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 6.0, + "pool": 6.0, + "read": 6.0, + "write": 6.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 2.0, + 4.0, + 6.0 + ] + }, + "borrowed_300:assess": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 300.0, + "pool": 300.0, + "read": 300.0, + "write": 300.0 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "borrowed_300:extract": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 300.0, + "pool": 300.0, + "read": 300.0, + "write": 300.0 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "borrowed_300:extract_timeout_9": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 9, + "pool": 9, + "read": 9, + "write": 9 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "borrowed_300:review_and_wait": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 300.0, + "pool": 300.0, + "read": 300.0, + "write": 300.0 + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 12.0, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 10.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 2.0, + "pool": 2.0, + "read": 2.0, + "write": 2.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [ + 10.0 + ] + }, + "borrowed_300:usage": { + "outcome": { + "result": "Usage" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 300.0, + "pool": 300.0, + "read": 300.0, + "write": 300.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [] + }, + "borrowed_300:wait": { + "outcome": { + "code": "", + "error": "LenzTimeoutError", + "idempotency_key": null, + "message": "wait timed out after 12s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 12.0, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 10.0, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 6.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "python-httpx/0.28.1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 6.0, + "pool": 6.0, + "read": 6.0, + "write": 6.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 2.0, + 4.0, + 6.0 + ] + }, + "cancel": { + "outcome": { + "result": "CancelResult" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Content-Length", + "0" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/3f2a9c1e5b7d4a608c1d2e3f4a5b6c7d/cancel" + } + ], + "sleeps": [] + }, + "cancel_citecheck": { + "outcome": { + "result": "Citecheck" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Content-Length", + "0" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/citechecks/12bbbf65/cancel" + } + ], + "sleeps": [] + }, + "cancel_review": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Content-Length", + "0" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/reviews/d6b2bd72/cancel" + } + ], + "sleeps": [] + }, + "citecheck_and_wait": { + "outcome": { + "result": "Citecheck" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "22" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/citecheck" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/citechecks/12bbbf65" + } + ], + "sleeps": [] + }, + "citecheck_pairs": { + "outcome": { + "result": "CitecheckStarted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"pairs\":[{\"statement\":\"S.\",\"url\":\"https://e.x/s\"}]}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "52" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/citecheck" + } + ], + "sleeps": [] + }, + "citecheck_text": { + "outcome": { + "result": "CitecheckStarted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"max_citations\":2}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "40" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/citecheck" + } + ], + "sleeps": [] + }, + "error_404": { + "outcome": { + "code": "", + "error": "LenzNotFoundError", + "idempotency_key": null, + "message": "nope", + "status_code": 404 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [] + }, + "extract": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 150.0, + "pool": 150.0, + "read": 150.0, + "write": 150.0 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "extract_options": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\",\"language\":\"it\",\"focus\":\"figures\",\"locate\":true}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "63" + ] + ], + "method": "POST", + "timeout": { + "connect": 150.0, + "pool": 150.0, + "read": 150.0, + "write": 150.0 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "extract_timeout_5": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 5, + "pool": 5, + "read": 5, + "write": 5 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "extract_timeout_obj": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 7, + "pool": 7, + "read": 300, + "write": 7 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "get_citecheck": { + "outcome": { + "result": "Citecheck" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/citechecks/12bbbf65" + } + ], + "sleeps": [] + }, + "get_review": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [] + }, + "get_review_issues": { + "outcome": { + "result": "ReviewIssues" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9?view=issues" + } + ], + "sleeps": [] + }, + "get_status": { + "outcome": { + "result": "TaskStatus" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [] + }, + "keyless_refused": { + "outcome": { + "code": "", + "error": "LenzAuthError", + "idempotency_key": null, + "message": "API key required", + "status_code": 0 + }, + "requests": [], + "sleeps": [] + }, + "library_iter": { + "outcome": { + "result": [ + "LibraryItem", + "LibraryItem" + ] + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/library?page=1&sort=recent&search=x&domain=&entity=" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/library?page=2&sort=recent&search=x&domain=&entity=" + } + ], + "sleeps": [] + }, + "library_list": { + "outcome": { + "result": "LibraryList" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/library?page=1&sort=recent&search=&domain=&entity=" + } + ], + "sleeps": [] + }, + "library_list_filters": { + "outcome": { + "result": "LibraryList" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/library?page=2&sort=most_true&search=x&domain=&entity=&curated=trivia&verdict=True" + } + ], + "sleeps": [] + }, + "max_retries_0_503": { + "outcome": { + "code": "", + "error": "LenzAPIError", + "idempotency_key": null, + "message": "busy", + "status_code": 503 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [] + }, + "max_retries_1_503": { + "outcome": { + "code": "", + "error": "LenzAPIError", + "idempotency_key": "", + "message": "busy", + "status_code": 503 + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 100.0, + "pool": 100.0, + "read": 100.0, + "write": 100.0 + }, + "url": "https://lenz.io/api/v1/assess" + }, + { + "at": 1.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 100.0, + "pool": 100.0, + "read": 100.0, + "write": 100.0 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [ + 1.0 + ] + }, + "retries_exhausted_503": { + "outcome": { + "code": "", + "error": "LenzAPIError", + "idempotency_key": null, + "message": "busy", + "status_code": 503 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + }, + { + "at": 1.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + }, + { + "at": 3.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + }, + { + "at": 7.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [ + 1.0, + 2.0, + 4.0 + ] + }, + "retries_exhausted_transport": { + "outcome": { + "code": "", + "error": "LenzConnectionError", + "idempotency_key": null, + "message": "GET /ask/v1 failed after 4 attempts: connection refused", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/ask/v1" + }, + { + "at": 1.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/ask/v1" + }, + { + "at": 3.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/ask/v1" + }, + { + "at": 7.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/ask/v1" + } + ], + "sleeps": [ + 1.0, + 2.0, + 4.0 + ] + }, + "retry_409_conflict": { + "outcome": { + "result": "TaskAccepted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\",\"source_url\":\"\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "29" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify" + }, + { + "at": 2.0, + "body": "{\"text\":\"A.\",\"source_url\":\"\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "29" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify" + } + ], + "sleeps": [ + 2 + ] + }, + "retry_429_stated": { + "outcome": { + "result": "Usage" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [ + 2 + ] + }, + "retry_503": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 100.0, + "pool": 100.0, + "read": 100.0, + "write": 100.0 + }, + "url": "https://lenz.io/api/v1/assess" + }, + { + "at": 1.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 100.0, + "pool": 100.0, + "read": 100.0, + "write": 100.0 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [ + 1.0 + ] + }, + "retry_transport": { + "outcome": { + "result": "TaskStatus" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 1.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 1.0 + ] + }, + "review": { + "outcome": { + "result": "ReviewStarted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/review" + } + ], + "sleeps": [] + }, + "review_and_wait": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 10.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 20.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [ + 10.0, + 10.0 + ] + }, + "review_and_wait_times_out": { + "outcome": { + "code": "", + "error": "ReviewTimeout", + "idempotency_key": "", + "message": "review_and_wait timed out after 25s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 25.0, + "pool": 25.0, + "read": 25.0, + "write": 25.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 10.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 15.0, + "pool": 15.0, + "read": 15.0, + "write": 15.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 20.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 5.0, + "pool": 5.0, + "read": 5.0, + "write": 5.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [ + 10.0, + 10.0, + 5.0 + ] + }, + "review_and_wait_zero": { + "outcome": { + "code": "", + "error": "ReviewTimeout", + "idempotency_key": "", + "message": "review_and_wait timed out after 0s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [] + }, + "review_options": { + "outcome": { + "result": "ReviewStarted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\",\"escalate\":{\"verdicts\":[\"False\"],\"max_citations\":3,\"suggest_edits\":true}}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "118" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/review" + } + ], + "sleeps": [] + }, + "select": { + "outcome": { + "result": "BatchAccepted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"texts\":[\"A.\",\"B.\"]}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "21" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/t1/select" + } + ], + "sleeps": [] + }, + "timeout_120:assess": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 120.0, + "pool": 120.0, + "read": 120.0, + "write": 120.0 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "timeout_120:extract": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 150.0, + "pool": 150.0, + "read": 150.0, + "write": 150.0 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_120:extract_timeout_9": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 9, + "pool": 9, + "read": 9, + "write": 9 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_120:review_and_wait": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 120.0, + "pool": 120.0, + "read": 120.0, + "write": 120.0 + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 12.0, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 10.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 2.0, + "pool": 2.0, + "read": 2.0, + "write": 2.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [ + 10.0 + ] + }, + "timeout_120:usage": { + "outcome": { + "result": "Usage" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 120.0, + "pool": 120.0, + "read": 120.0, + "write": 120.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [] + }, + "timeout_120:wait": { + "outcome": { + "code": "", + "error": "LenzTimeoutError", + "idempotency_key": null, + "message": "wait timed out after 12s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 12.0, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 10.0, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 6.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 6.0, + "pool": 6.0, + "read": 6.0, + "write": 6.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 2.0, + 4.0, + 6.0 + ] + }, + "timeout_200_read_5:assess": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 100.0, + "pool": 100.0, + "read": 100.0, + "write": 100.0 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "timeout_200_read_5:extract": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 150.0, + "pool": 150.0, + "read": 150.0, + "write": 150.0 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_200_read_5:extract_timeout_9": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 9, + "pool": 9, + "read": 9, + "write": 9 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_200_read_5:review_and_wait": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 200, + "pool": 200, + "read": 5, + "write": 200 + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 5, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 10.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 2.0, + "pool": 2.0, + "read": 2.0, + "write": 2.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [ + 10.0 + ] + }, + "timeout_200_read_5:usage": { + "outcome": { + "result": "Usage" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 200, + "pool": 200, + "read": 5, + "write": 200 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [] + }, + "timeout_200_read_5:wait": { + "outcome": { + "code": "", + "error": "LenzTimeoutError", + "idempotency_key": null, + "message": "wait timed out after 12s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 5, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 5, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 6.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 6.0, + "pool": 6.0, + "read": 5, + "write": 6.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 2.0, + 4.0, + 6.0 + ] + }, + "timeout_30_read_none:assess": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 30, + "pool": 30, + "read": null, + "write": 30 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "timeout_30_read_none:extract": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 30, + "pool": 30, + "read": null, + "write": 30 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_30_read_none:extract_timeout_9": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 9, + "pool": 9, + "read": 9, + "write": 9 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_30_read_none:review_and_wait": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 30, + "pool": 30, + "read": null, + "write": 30 + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 12.0, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 10.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 2.0, + "pool": 2.0, + "read": 2.0, + "write": 2.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [ + 10.0 + ] + }, + "timeout_30_read_none:usage": { + "outcome": { + "result": "Usage" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30, + "pool": 30, + "read": null, + "write": 30 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [] + }, + "timeout_30_read_none:wait": { + "outcome": { + "code": "", + "error": "LenzTimeoutError", + "idempotency_key": null, + "message": "wait timed out after 12s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 12.0, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 10.0, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 6.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 6.0, + "pool": 6.0, + "read": 6.0, + "write": 6.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 2.0, + 4.0, + 6.0 + ] + }, + "timeout_5_read_200:assess": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": 5, + "pool": 5, + "read": 200, + "write": 5 + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "timeout_5_read_200:extract": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 5, + "pool": 5, + "read": 200, + "write": 5 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_5_read_200:extract_timeout_9": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 9, + "pool": 9, + "read": 9, + "write": 9 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_5_read_200:review_and_wait": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": 5, + "pool": 5, + "read": 200, + "write": 5 + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 5, + "pool": 5, + "read": 12.0, + "write": 5 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 10.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 2.0, + "pool": 2.0, + "read": 2.0, + "write": 2.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [ + 10.0 + ] + }, + "timeout_5_read_200:usage": { + "outcome": { + "result": "Usage" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 5, + "pool": 5, + "read": 200, + "write": 5 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [] + }, + "timeout_5_read_200:wait": { + "outcome": { + "code": "", + "error": "LenzTimeoutError", + "idempotency_key": null, + "message": "wait timed out after 12s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 5, + "pool": 5, + "read": 12.0, + "write": 5 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 5, + "pool": 5, + "read": 10.0, + "write": 5 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 6.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 5, + "pool": 5, + "read": 6.0, + "write": 5 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 2.0, + 4.0, + 6.0 + ] + }, + "timeout_none:assess": { + "outcome": { + "result": "AssessResponse" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "13" + ] + ], + "method": "POST", + "timeout": { + "connect": null, + "pool": null, + "read": null, + "write": null + }, + "url": "https://lenz.io/api/v1/assess" + } + ], + "sleeps": [] + }, + "timeout_none:extract": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": null, + "pool": null, + "read": null, + "write": null + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_none:extract_timeout_9": { + "outcome": { + "result": "ExtractedClaims" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Doc.\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "15" + ] + ], + "method": "POST", + "timeout": { + "connect": 9, + "pool": 9, + "read": 9, + "write": 9 + }, + "url": "https://lenz.io/api/v1/extract" + } + ], + "sleeps": [] + }, + "timeout_none:review_and_wait": { + "outcome": { + "result": "ReviewFull" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"Draft text.\",\"visibility\":\"private\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "45" + ] + ], + "method": "POST", + "timeout": { + "connect": null, + "pool": null, + "read": null, + "write": null + }, + "url": "https://lenz.io/api/v1/review" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 12.0, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + }, + { + "at": 10.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 2.0, + "pool": 2.0, + "read": 2.0, + "write": 2.0 + }, + "url": "https://lenz.io/api/v1/reviews/442b6aa9" + } + ], + "sleeps": [ + 10.0 + ] + }, + "timeout_none:usage": { + "outcome": { + "result": "Usage" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": null, + "pool": null, + "read": null, + "write": null + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [] + }, + "timeout_none:wait": { + "outcome": { + "code": "", + "error": "LenzTimeoutError", + "idempotency_key": null, + "message": "wait timed out after 12s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 12.0, + "pool": 12.0, + "read": 12.0, + "write": 12.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 10.0, + "pool": 10.0, + "read": 10.0, + "write": 10.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 6.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 6.0, + "pool": 6.0, + "read": 6.0, + "write": 6.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 2.0, + 4.0, + 6.0 + ] + }, + "usage": { + "outcome": { + "result": "Usage" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/me/usage" + } + ], + "sleeps": [] + }, + "verifications_delete": { + "outcome": { + "result": "bool" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "DELETE", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications/v1" + } + ], + "sleeps": [] + }, + "verifications_delete_404": { + "outcome": { + "result": "bool" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "DELETE", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications/v1" + } + ], + "sleeps": [] + }, + "verifications_get": { + "outcome": { + "result": "Verification" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications/v1" + } + ], + "sleeps": [] + }, + "verifications_get_certificate": { + "outcome": { + "result": "Certificate" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications/v1/certificate" + } + ], + "sleeps": [] + }, + "verifications_get_keyless": { + "outcome": { + "result": "Verification" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications/v1" + } + ], + "sleeps": [] + }, + "verifications_iter": { + "outcome": { + "result": [ + "VerificationListItem", + "VerificationListItem" + ] + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications?page=1" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications?page=2" + } + ], + "sleeps": [] + }, + "verifications_list": { + "outcome": { + "result": "VerificationList" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications?page=1" + } + ], + "sleeps": [] + }, + "verifications_list_p2": { + "outcome": { + "result": "VerificationList" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications?page=2" + } + ], + "sleeps": [] + }, + "verifications_related": { + "outcome": { + "result": "RelatedVerifications" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verifications/v1/related?limit=3" + } + ], + "sleeps": [] + }, + "verify_and_wait": { + "outcome": { + "result": "Verification" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\",\"source_url\":\"\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "29" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 3.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 7.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 3.0, + 4.0 + ] + }, + "verify_and_wait_times_out": { + "outcome": { + "code": "", + "error": "LenzTimeoutError", + "idempotency_key": "", + "message": "wait timed out after 5s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\",\"source_url\":\"\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "29" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 5.0, + "pool": 5.0, + "read": 5.0, + "write": 5.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 3.0, + "pool": 3.0, + "read": 3.0, + "write": 3.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 2.0, + 3.0 + ] + }, + "verify_batch": { + "outcome": { + "result": "BatchAccepted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"claims\":[{\"text\":\"A.\"},{\"text\":\"B.\"}],\"language\":\"de\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "56" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/batch" + } + ], + "sleeps": [] + }, + "verify_batch_and_wait": { + "outcome": { + "result": [ + "BatchItemResult", + "BatchItemResult" + ] + }, + "requests": [ + { + "at": 0.0, + "body": "{\"claims\":[{\"text\":\"A.\"},{\"text\":\"B.\"}]}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "40" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/batch" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t2" + }, + { + "at": 3.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 3.0 + ] + }, + "verify_no_key": { + "outcome": { + "result": "TaskAccepted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\",\"source_url\":\"\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "29" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify" + } + ], + "sleeps": [] + }, + "verify_options": { + "outcome": { + "result": "TaskAccepted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\",\"source_url\":\"https://e.x/a\",\"webhook_url\":\"https://e.x/h\",\"language\":\"es\",\"visibility\":\"unlisted\",\"depth\":\"low\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "126" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify" + } + ], + "sleeps": [] + }, + "verify_pinned": { + "outcome": { + "result": "TaskAccepted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\",\"source_url\":\"\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "pinned-key-1" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "29" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify" + } + ], + "sleeps": [] + }, + "verify_random_key": { + "outcome": { + "result": "TaskAccepted" + }, + "requests": [ + { + "at": 0.0, + "body": "{\"text\":\"A.\",\"source_url\":\"\"}", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Idempotency-Key", + "" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ], + [ + "Content-Length", + "29" + ] + ], + "method": "POST", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify" + } + ], + "sleeps": [] + }, + "wait_negative": { + "outcome": { + "result": "Verification" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [] + }, + "wait_poll_errors": { + "outcome": { + "result": "Verification" + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 2.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 6.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + }, + { + "at": 14.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [ + 2.0, + 4.0, + 8.0 + ] + }, + "wait_zero": { + "outcome": { + "code": "", + "error": "LenzTimeoutError", + "idempotency_key": null, + "message": "wait timed out after 0s", + "status_code": 0 + }, + "requests": [ + { + "at": 0.0, + "body": "", + "headers": [ + [ + "Host", + "lenz.io" + ], + [ + "Accept-Encoding", + "" + ], + [ + "Connection", + "keep-alive" + ], + [ + "User-Agent", + "" + ], + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer lenz_00000000000000000000000000000000" + ], + [ + "Content-Type", + "application/json" + ], + [ + "X-Lenz-API-Version", + "2026-10-11" + ] + ], + "method": "GET", + "timeout": { + "connect": 30.0, + "pool": 30.0, + "read": 30.0, + "write": 30.0 + }, + "url": "https://lenz.io/api/v1/verify/status/t1" + } + ], + "sleeps": [] + } +} diff --git a/tests/freeze_cases.py b/tests/freeze_cases.py new file mode 100644 index 0000000..67c48f6 --- /dev/null +++ b/tests/freeze_cases.py @@ -0,0 +1,350 @@ +"""Every public call form, with scripted answers, for the request freeze. + +Each case is ``(name, client, call, answers)``: ``client`` names a factory in +``freeze_harness.CLIENTS``, ``call`` runs the public method, ``answers`` maps +``(method, path)`` to the scripted answers. ``test_request_freeze.py`` records +what each case sends (URL, raw body, ordered headers, the four timeout +components, sleeps) and compares it with ``fixtures/freeze/requests.json``. +""" + +from __future__ import annotations + +import json +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import httpx +from freeze_harness import PINNED, Answer + +from lenz_io import Lenz + +_CONTRACT = Path(__file__).parent / "fixtures" / "contract" + + +def _load(name: str) -> Any: + return json.loads((_CONTRACT / name).read_text()) + + +TASK = "t1" +ACCEPTED = (200, {"task_id": TASK, "claim_text": "A."}) +DONE = (200, {"status": "completed", "task_id": TASK, "result": {"verification_id": "v1", "claim": "A."}}) +DONE2 = (200, {"status": "completed", "task_id": "t2", "result": {"verification_id": "v2", "claim": "B."}}) +RUNNING = (200, {"status": "processing", "task_id": TASK, "progress": {"step": "research", "poll_after_seconds": 3}}) +RUNNING_NO_HINT = (200, {"status": "processing", "task_id": TASK, "progress": {"step": "research"}}) +BATCH = (200, {"batch_id": "b1", "items": [{"task_id": TASK, "claim": "A."}, {"task_id": "t2", "claim": "B."}]}) +EXTRACTED = (200, {"claims": [], "status": "ok"}) +ASSESSED = (200, {"claims": []}) +SELECTED = (200, {"items": []}) +REVIEW_DONE = _load("review_completed.json") +REVIEW_RUNNING = _load("review_assessing.json") +RID = REVIEW_DONE["review_id"] +CHECK_DONE = _load("citecheck_completed.json") +CID = CHECK_DONE["citecheck_id"] +CANCEL_TASK = _load("cancel_verify_cancelled.json") +CANCEL_REVIEW = _load("cancel_review_cancelled.json") +CANCEL_CHECK = _load("cancel_citecheck_cancelled.json") +_ITEM = _load("verifications_list.json")["items"][0] +PAGE_1 = (200, {"items": [_ITEM], "total": 2, "page": 1, "page_size": 1}) +PAGE_2 = (200, {"items": [_ITEM], "total": 2, "page": 2, "page_size": 1}) +LIB_ITEM = {"verification_id": "lib1", "claim": "A."} +LIB_1 = (200, {"items": [LIB_ITEM], "total": 2, "page": 1, "page_size": 1}) +LIB_2 = (200, {"items": [LIB_ITEM], "total": 2, "page": 2, "page_size": 1}) +DETAIL = (200, _load("verifications_detail.json")) +CERT = (200, _load("certificate.json")) +USAGE = (200, _load("usage.json")) +ASK_REPLY = (200, {"role": "expert", "content": "Because."}) +ASK_HISTORY = (200, {"messages": []}) +RELATED = (200, {"items": []}) +DRAFT = "Draft text." + +S503 = (503, {"detail": "busy"}) +CONFLICT = (409, {"code": "idempotency_conflict", "detail": "in flight"}, {"Retry-After": "2"}) +LIMITED = (429, {"code": "rate_limited", "detail": "slow down"}, {"Retry-After": "2"}) +NOT_FOUND = (404, {"code": "not_found", "detail": "nope"}) + + +def _drop() -> Exception: + return httpx.ConnectError("connection refused") + + +Case = tuple[str, str, Callable[[Lenz], Any], dict[tuple[str, str], list[Answer]]] + +CASES: list[Case] = [ + # ── submits ── + ("verify_pinned", "default", lambda c: c.verify("A.", idempotency_key=PINNED), {("POST", "/verify"): [ACCEPTED]}), + ("verify_random_key", "default", lambda c: c.verify(claim="A."), {("POST", "/verify"): [ACCEPTED]}), + ("verify_no_key", "default", lambda c: c.verify("A.", idempotency=False), {("POST", "/verify"): [ACCEPTED]}), + ( + "verify_options", + "default", + lambda c: c.verify( + "A.", + language="es", + visibility="unlisted", + depth="low", + source_url="https://e.x/a", + webhook_url="https://e.x/h", + ), + {("POST", "/verify"): [ACCEPTED]}, + ), + ( + "verify_batch", + "default", + lambda c: c.verify_batch(claims=[{"claim": "A."}, {"text": "B."}], idempotency_key=PINNED, language="de"), + {("POST", "/verify/batch"): [BATCH]}, + ), + ("extract", "default", lambda c: c.extract(text="Doc."), {("POST", "/extract"): [EXTRACTED]}), + ( + "extract_options", + "default", + lambda c: c.extract(text="Doc.", language="it", focus="figures", locate=True, idempotency_key=PINNED), + {("POST", "/extract"): [EXTRACTED]}, + ), + ("extract_timeout_5", "default", lambda c: c.extract(text="Doc.", timeout=5), {("POST", "/extract"): [EXTRACTED]}), + ( + "extract_timeout_obj", + "default", + lambda c: c.extract(text="Doc.", timeout=httpx.Timeout(7, read=300)), # type: ignore[arg-type] + {("POST", "/extract"): [EXTRACTED]}, + ), + ("assess_claim", "default", lambda c: c.assess("A."), {("POST", "/assess"): [ASSESSED]}), + ( + "assess_claims", + "default", + lambda c: c.assess(claims=["A.", "B."], language="auto", suggest_rewrite=True, idempotency_key=PINNED), + {("POST", "/assess"): [ASSESSED]}, + ), + ("assess_timeout_20", "default", lambda c: c.assess("A.", timeout=20), {("POST", "/assess"): [ASSESSED]}), + ( + "select", + "default", + lambda c: c.select(TASK, claims=["A.", "B."]), + {("POST", f"/verify/{TASK}/select"): [SELECTED]}, + ), + ("get_status", "default", lambda c: c.get_status(TASK), {("GET", f"/verify/status/{TASK}"): [DONE]}), + ( + "cancel", + "default", + lambda c: c.cancel(CANCEL_TASK["task_id"]), + {("POST", f"/verify/{CANCEL_TASK['task_id']}/cancel"): [(200, CANCEL_TASK)]}, + ), + ( + "review", + "default", + lambda c: c.review(DRAFT), + {("POST", "/review"): [(202, {"review_id": RID, "status": "queued"})]}, + ), + ( + "review_options", + "default", + lambda c: c.review(DRAFT, verdicts=["False"], max_citations=3, suggest_edits=True, idempotency_key=PINNED), + {("POST", "/review"): [(202, {"review_id": RID, "status": "queued"})]}, + ), + ("get_review", "default", lambda c: c.get_review(RID), {("GET", f"/reviews/{RID}"): [(200, REVIEW_DONE)]}), + ( + "get_review_issues", + "default", + lambda c: c.get_review(RID, view="issues"), + {("GET", f"/reviews/{RID}"): [(200, _load("review_completed_issues.json"))]}, + ), + ( + "cancel_review", + "default", + lambda c: c.cancel_review(CANCEL_REVIEW["review_id"]), + {("POST", f"/reviews/{CANCEL_REVIEW['review_id']}/cancel"): [(200, CANCEL_REVIEW)]}, + ), + ( + "citecheck_text", + "default", + lambda c: c.citecheck(DRAFT, max_citations=2), + {("POST", "/citecheck"): [(202, {"citecheck_id": CID, "status": "queued"})]}, + ), + ( + "citecheck_pairs", + "default", + lambda c: c.citecheck(pairs=[{"statement": "S.", "url": "https://e.x/s"}], idempotency_key=PINNED), + {("POST", "/citecheck"): [(202, {"citecheck_id": CID, "status": "queued"})]}, + ), + ("get_citecheck", "default", lambda c: c.get_citecheck(CID), {("GET", f"/citechecks/{CID}"): [(200, CHECK_DONE)]}), + ( + "cancel_citecheck", + "default", + lambda c: c.cancel_citecheck(CID), + {("POST", f"/citechecks/{CID}/cancel"): [(200, CANCEL_CHECK)]}, + ), + ("usage", "default", lambda c: c.usage(), {("GET", "/me/usage"): [USAGE]}), + # ── namespaces ── + ("verifications_list", "default", lambda c: c.verifications.list(), {("GET", "/verifications"): [PAGE_1]}), + ("verifications_list_p2", "default", lambda c: c.verifications.list(page=2), {("GET", "/verifications"): [PAGE_2]}), + ("verifications_iter", "default", lambda c: c.verifications.iter(), {("GET", "/verifications"): [PAGE_1, PAGE_2]}), + ("verifications_get", "default", lambda c: c.verifications.get("v1"), {("GET", "/verifications/v1"): [DETAIL]}), + ( + "verifications_get_keyless", + "keyless", + lambda c: c.verifications.get("v1"), + {("GET", "/verifications/v1"): [DETAIL]}, + ), + ( + "verifications_get_certificate", + "default", + lambda c: c.verifications.get_certificate("v1"), + {("GET", "/verifications/v1/certificate"): [CERT]}, + ), + ( + "verifications_delete", + "default", + lambda c: c.verifications.delete("v1"), + {("DELETE", "/verifications/v1"): [(204, None)]}, + ), + ( + "verifications_delete_404", + "default", + lambda c: c.verifications.delete("v1"), + {("DELETE", "/verifications/v1"): [NOT_FOUND]}, + ), + ( + "verifications_related", + "default", + lambda c: c.verifications.related("v1", limit=3), + {("GET", "/verifications/v1/related"): [RELATED]}, + ), + ("ask_history", "default", lambda c: c.ask.history("v1"), {("GET", "/ask/v1"): [ASK_HISTORY]}), + ( + "ask_send", + "default", + lambda c: c.ask.send("v1", message="Why?", language="auto"), + {("POST", "/ask/v1"): [ASK_REPLY]}, + ), + ("ask_reset", "default", lambda c: c.ask.reset("v1"), {("DELETE", "/ask/v1"): [(204, None)]}), + ("library_list", "default", lambda c: c.library.list(), {("GET", "/library"): [LIB_1]}), + ( + "library_list_filters", + "keyless", + lambda c: c.library.list(page=2, sort="most_true", search="x", curated=["trivia"], verdict="True"), + {("GET", "/library"): [LIB_2]}, + ), + ("library_iter", "default", lambda c: c.library.iter(search="x"), {("GET", "/library"): [LIB_1, LIB_2]}), + # ── waits ── + ( + "verify_and_wait", + "default", + lambda c: c.verify_and_wait("A.", idempotency_key=PINNED), + {("POST", "/verify"): [ACCEPTED], ("GET", f"/verify/status/{TASK}"): [RUNNING, RUNNING_NO_HINT, DONE]}, + ), + ( + "verify_and_wait_times_out", + "default", + lambda c: c.verify_and_wait("A.", timeout=5), + {("POST", "/verify"): [ACCEPTED], ("GET", f"/verify/status/{TASK}"): [RUNNING_NO_HINT]}, + ), + ("wait_zero", "default", lambda c: c.wait(TASK, timeout=0), {("GET", f"/verify/status/{TASK}"): [RUNNING]}), + ("wait_negative", "default", lambda c: c.wait(TASK, timeout=-1), {("GET", f"/verify/status/{TASK}"): [DONE]}), + ( + "wait_poll_errors", + "default", + lambda c: c.wait(TASK, timeout=60), + {("GET", f"/verify/status/{TASK}"): [S503, LIMITED, _drop(), DONE]}, + ), + ( + "verify_batch_and_wait", + "default", + lambda c: c.verify_batch_and_wait(claims=[{"claim": "A."}, {"claim": "B."}], idempotency_key=PINNED), + { + ("POST", "/verify/batch"): [BATCH], + ("GET", f"/verify/status/{TASK}"): [RUNNING, DONE], + ("GET", "/verify/status/t2"): [NOT_FOUND], + }, + ), + ( + "review_and_wait", + "default", + lambda c: c.review_and_wait(DRAFT, idempotency_key=PINNED), + { + ("POST", "/review"): [(202, {"review_id": RID, "status": "queued"})], + ("GET", f"/reviews/{RID}"): [(200, REVIEW_RUNNING), S503, (200, REVIEW_DONE)], + }, + ), + ( + "review_and_wait_zero", + "default", + lambda c: c.review_and_wait(DRAFT, timeout=0), + { + ("POST", "/review"): [(202, {"review_id": RID, "status": "queued"})], + ("GET", f"/reviews/{RID}"): [(200, REVIEW_RUNNING)], + }, + ), + ( + "review_and_wait_times_out", + "default", + lambda c: c.review_and_wait(DRAFT, timeout=25), + { + ("POST", "/review"): [(202, {"review_id": RID, "status": "queued"})], + ("GET", f"/reviews/{RID}"): [(200, REVIEW_RUNNING)], + }, + ), + ( + "citecheck_and_wait", + "default", + lambda c: c.citecheck_and_wait(DRAFT), + { + ("POST", "/citecheck"): [(202, {"citecheck_id": CID, "status": "queued"})], + ("GET", f"/citechecks/{CID}"): [(200, CHECK_DONE)], + }, + ), + # ── retries and errors ── + ("retry_503", "default", lambda c: c.assess("A.", idempotency_key=PINNED), {("POST", "/assess"): [S503, ASSESSED]}), + ("retry_429_stated", "default", lambda c: c.usage(), {("GET", "/me/usage"): [LIMITED, USAGE]}), + ( + "retry_409_conflict", + "default", + lambda c: c.verify("A.", idempotency_key=PINNED), + {("POST", "/verify"): [CONFLICT, ACCEPTED]}, + ), + ("retry_transport", "default", lambda c: c.get_status(TASK), {("GET", f"/verify/status/{TASK}"): [_drop(), DONE]}), + ("retries_exhausted_503", "default", lambda c: c.usage(), {("GET", "/me/usage"): [S503]}), + ("retries_exhausted_transport", "default", lambda c: c.ask.history("v1"), {("GET", "/ask/v1"): [_drop()]}), + ("max_retries_0_503", "max_retries_0", lambda c: c.usage(), {("GET", "/me/usage"): [S503]}), + ("max_retries_1_503", "max_retries_1", lambda c: c.assess("A."), {("POST", "/assess"): [S503]}), + ("error_404", "default", lambda c: c.get_status(TASK), {("GET", f"/verify/status/{TASK}"): [NOT_FOUND]}), + ("keyless_refused", "keyless", lambda c: c.usage(), {}), +] + +# The per-attempt timeout under every client configuration: the floored calls, +# a plain call, and a wait's polls (capped by what is left of the wait). +_TIMEOUT_CLIENTS = [ + "timeout_none", + "timeout_5_read_200", + "timeout_200_read_5", + "timeout_30_read_none", + "timeout_120", + "borrowed_10", + "borrowed_300", +] +for _client in _TIMEOUT_CLIENTS: + CASES += [ + (f"{_client}:extract", _client, lambda c: c.extract(text="Doc."), {("POST", "/extract"): [EXTRACTED]}), + (f"{_client}:assess", _client, lambda c: c.assess("A."), {("POST", "/assess"): [ASSESSED]}), + ( + f"{_client}:extract_timeout_9", + _client, + lambda c: c.extract(text="Doc.", timeout=9), + {("POST", "/extract"): [EXTRACTED]}, + ), + (f"{_client}:usage", _client, lambda c: c.usage(), {("GET", "/me/usage"): [USAGE]}), + ( + f"{_client}:wait", + _client, + lambda c: c.wait(TASK, timeout=12), + {("GET", f"/verify/status/{TASK}"): [RUNNING_NO_HINT, RUNNING_NO_HINT, RUNNING_NO_HINT, DONE]}, + ), + ( + f"{_client}:review_and_wait", + _client, + lambda c: c.review_and_wait(DRAFT, timeout=12, idempotency_key=PINNED), + { + ("POST", "/review"): [(202, {"review_id": RID, "status": "queued"})], + ("GET", f"/reviews/{RID}"): [(200, REVIEW_RUNNING), (200, REVIEW_DONE)], + }, + ), + ] diff --git a/tests/freeze_harness.py b/tests/freeze_harness.py new file mode 100644 index 0000000..54e9301 --- /dev/null +++ b/tests/freeze_harness.py @@ -0,0 +1,160 @@ +"""A recorder for what the client puts on the wire, for the request freeze +(``test_request_freeze.py``) and the request-option tests. + +Every request is recorded with its method, URL (query string as sent), raw +body, ordered header pairs and the four ``httpx.Timeout`` components the +client passed to httpx; every sleep with its length, under a fake clock that +only moves when the client sleeps. Responses are scripted per (method, path). +""" + +from __future__ import annotations + +import re +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from dataclasses import dataclass, field +from typing import Any + +import httpx +import pytest +import respx + +from lenz_io import Lenz +from lenz_io import client as client_module + +BASE = "https://lenz.io/api/v1" +API_KEY = "lenz_" + "0" * 32 +PINNED = "pinned-key-1" +_RANDOM_KEY = re.compile(r"^[0-9a-f]{32}$") + +#: A scripted answer: ``(status, json_body)``, ``(status, json_body, headers)`` +#: or an exception instance to raise from the transport. +Answer = Any + + +@dataclass +class FakeClock: + now: float = 1000.0 + sleeps: list[float] = field(default_factory=list) + + def monotonic(self) -> float: + return self.now + + def sleep(self, seconds: float) -> None: + self.sleeps.append(seconds) + self.now += seconds + + +class _FakeTime: + """Stands in for the ``time`` module inside ``lenz_io.client`` only.""" + + def __init__(self, clock: FakeClock) -> None: + self.monotonic = clock.monotonic + self.sleep = clock.sleep + + +def _mask_headers(raw: list[tuple[bytes, bytes]]) -> list[list[str]]: + out = [] + for name_b, value_b in raw: + name, value = name_b.decode("latin-1"), value_b.decode("latin-1") + lower = name.lower() + if lower == "user-agent" and value.startswith("lenz-io-python/"): + value = "" + elif lower == "accept-encoding": + value = "" # depends on which decoders are installed + elif lower == "idempotency-key" and value != PINNED and _RANDOM_KEY.match(value): + value = "" + out.append([name, value]) + return out + + +@dataclass +class Recording: + requests: list[dict[str, Any]] = field(default_factory=list) + clock: FakeClock = field(default_factory=FakeClock) + + def summary(self) -> list[dict[str, Any]]: + return self.requests + + +class Script: + """Scripted answers per (method, path below the base URL). The last answer + of a list repeats.""" + + def __init__(self, answers: dict[tuple[str, str], list[Answer]], recording: Recording) -> None: + self._answers = {k: list(v) for k, v in answers.items()} + self._rec = recording + + def __call__(self, request: httpx.Request) -> httpx.Response: + path = request.url.path + if path.startswith("/api/v1"): + path = path[len("/api/v1") :] + timeout = request.extensions.get("timeout") or {} + self._rec.requests.append( + { + "method": request.method, + "url": str(request.url), + "headers": _mask_headers(request.headers.raw), + "body": request.content.decode("utf-8"), + "timeout": {k: timeout.get(k) for k in ("connect", "read", "write", "pool")}, + "at": round(self._rec.clock.now - 1000.0, 6), + } + ) + queue = self._answers.get((request.method, path)) + if not queue: + raise AssertionError(f"unscripted request {request.method} {path}") + answer = queue.pop(0) if len(queue) > 1 else queue[0] + if isinstance(answer, Exception): + raise answer + status, body, *rest = answer + headers = rest[0] if rest else {} + if body is None: + return httpx.Response(status, headers=headers) + return httpx.Response(status, json=body, headers=headers) + + +@contextmanager +def recording(monkeypatch: pytest.MonkeyPatch, answers: dict[tuple[str, str], list[Answer]]) -> Iterator[Recording]: + rec = Recording() + monkeypatch.setattr(client_module, "time", _FakeTime(rec.clock)) + with respx.mock(assert_all_called=False) as router: + router.route().mock(side_effect=Script(answers, rec)) + yield rec + + +def outcome(call: Callable[[], Any]) -> dict[str, Any]: + """What a call ended with: the result's type, or the error's class and + the fields a caller reads.""" + try: + result = call() + except Exception as exc: # every outcome is recorded + key = getattr(exc, "idempotency_key", None) + if isinstance(key, str) and key != PINNED and _RANDOM_KEY.match(key): + key = "" + return { + "error": type(exc).__name__, + "status_code": getattr(exc, "status_code", None), + "code": getattr(exc, "code", None), + "message": str(getattr(exc, "message", exc)), + "idempotency_key": key, + } + if isinstance(result, list): + return {"result": [type(r).__name__ for r in result]} + if isinstance(result, Iterator) or hasattr(result, "__next__"): + return {"result": [type(r).__name__ for r in result]} + return {"result": type(result).__name__} + + +CLIENTS: dict[str, Callable[[], Lenz]] = { + "default": lambda: Lenz(api_key=API_KEY), + "keyless": lambda: Lenz(api_key="", base_url=BASE), + "timeout_none": lambda: Lenz(api_key=API_KEY, timeout=None), + "timeout_5_read_200": lambda: Lenz(api_key=API_KEY, timeout=httpx.Timeout(5, read=200)), + "timeout_200_read_5": lambda: Lenz(api_key=API_KEY, timeout=httpx.Timeout(200, read=5)), + "timeout_30_read_none": lambda: Lenz(api_key=API_KEY, timeout=httpx.Timeout(30, read=None)), + "timeout_120": lambda: Lenz(api_key=API_KEY, timeout=120.0), + "borrowed_10": lambda: Lenz(api_key=API_KEY, http_client=httpx.Client(timeout=10.0)), + "borrowed_300": lambda: Lenz(api_key=API_KEY, http_client=httpx.Client(timeout=300.0)), + "max_retries_0": lambda: Lenz(api_key=API_KEY, max_retries=0), + "max_retries_1": lambda: Lenz(api_key=API_KEY, max_retries=1), +} diff --git a/tests/test_request_freeze.py b/tests/test_request_freeze.py new file mode 100644 index 0000000..a04ba28 --- /dev/null +++ b/tests/test_request_freeze.py @@ -0,0 +1,71 @@ +"""What every public call form puts on the wire, frozen. + +For each case in ``freeze_cases.py``: every request's URL (query string as +sent), raw body bytes, ordered header pairs and the four ``httpx.Timeout`` +components handed to httpx, the time of each request and every sleep under a +fake clock, and how the call ended. Compared with +``fixtures/freeze/requests.json``, recorded from the release before request +options existed: a call that passes no request option must send exactly this. + +A difference here is a behaviour change. Regenerating the file +(``python tests/test_request_freeze.py``) is for a deliberate change only, +and the diff of the JSON is the review. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from typing import Any + +import pytest +from freeze_cases import CASES +from freeze_harness import CLIENTS, outcome, recording + +GOLDEN = Path(__file__).parent / "fixtures" / "freeze" / "requests.json" + + +def observe(monkeypatch: pytest.MonkeyPatch, name: str) -> dict[str, Any]: + _, client_name, call, answers = next(c for c in CASES if c[0] == name) + with recording(monkeypatch, answers) as rec: + client = CLIENTS[client_name]() + try: + ended = outcome(lambda: call(client)) + finally: + client.close() + return {"requests": rec.requests, "sleeps": rec.clock.sleeps, "outcome": ended} + + +@pytest.fixture(autouse=True) +def _no_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("LENZ_API_KEY", raising=False) + monkeypatch.delenv("LENZ_BASE_URL", raising=False) + + +@pytest.mark.parametrize("name", [c[0] for c in CASES]) +def test_the_request_is_frozen(monkeypatch: pytest.MonkeyPatch, name: str) -> None: + golden = json.loads(GOLDEN.read_text()) + assert observe(monkeypatch, name) == golden[name] + + +def test_every_case_is_frozen() -> None: + assert sorted(json.loads(GOLDEN.read_text())) == sorted(c[0] for c in CASES) + + +if __name__ == "__main__": # pragma: no cover - regeneration, by hand only + import os + + os.environ.pop("LENZ_API_KEY", None) + os.environ.pop("LENZ_BASE_URL", None) + out: dict[str, Any] = {} + mp = pytest.MonkeyPatch() + try: + for case in CASES: + out[case[0]] = observe(mp, case[0]) + mp.undo() + finally: + mp.undo() + GOLDEN.parent.mkdir(parents=True, exist_ok=True) + GOLDEN.write_text(json.dumps(out, indent=1, sort_keys=True, ensure_ascii=False) + "\n") + sys.stdout.write(f"wrote {len(out)} cases to {GOLDEN}\n") From e2e141b93766e0cdee73b9c8db6c244df5e98646 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 23:50:21 +0300 Subject: [PATCH 56/67] feat: per-call timeout, max_retries and extra_headers, and with_options Every public method takes keyword-only request options: timeout (one HTTP attempt; None inherits), max_retries and extra_headers. The waits keep their timeout as the wait budget; their max_retries is the submit's, and wait() takes extra_headers only. Lenz.with_options returns a copy sharing the connection pool; a copy never closes it. Resolution is one pure function: call over copy over client, headers merged case-insensitively (None removes a copy's header, reserved names refused), the extract/assess floors on an inherited timeout only. One validation rule for every per-request timeout and retry count, the constructor included (ValueError before any request); wait budgets are unchanged. A call that passes no option sends exactly what it sent before (the request freeze is unchanged). The cancel signature test now allows the three keyword-only request options next to the id. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- src/lenz_io/__init__.py | 4 +- src/lenz_io/client.py | 880 +++++++++++++++++++++++++++---- tests/test_public_call_bodies.py | 9 +- tests/test_request_options.py | 778 +++++++++++++++++++++++++++ tests/test_smoke_staging.py | 10 + 5 files changed, 1577 insertions(+), 104 deletions(-) create mode 100644 tests/test_request_options.py diff --git a/src/lenz_io/__init__.py b/src/lenz_io/__init__.py index 11bc447..8703e84 100644 --- a/src/lenz_io/__init__.py +++ b/src/lenz_io/__init__.py @@ -54,7 +54,7 @@ __version__ = "0.0.0+local" # Public surface -from .client import API_VERSION, DEFAULT_BASE_URL, CitationPair, Lenz, VerifyBatchItem +from .client import API_VERSION, DEFAULT_BASE_URL, NOT_GIVEN, CitationPair, Lenz, NotGiven, VerifyBatchItem from .errors import ( MAX_RETRY_AFTER_SLEEP, CitecheckFailed, @@ -163,6 +163,7 @@ "API_VERSION", "DEFAULT_BASE_URL", "MAX_RETRY_AFTER_SLEEP", + "NOT_GIVEN", "AskHistory", "AskMessage", "AskReply", @@ -222,6 +223,7 @@ "LenzWebhooks", "LibraryItem", "LibraryList", + "NotGiven", "Position", "Progress", "RelatedVerifications", diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 18ef0b2..1a3f3f1 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -79,13 +79,16 @@ from __future__ import annotations import builtins +import copy import logging +import math import os import time import uuid -from collections.abc import Callable, Iterator, Sequence +from collections.abc import Callable, Iterator, Mapping, Sequence from contextlib import contextmanager -from typing import Any, Literal, TypedDict, TypeVar, overload +from dataclasses import dataclass +from typing import Any, Final, Literal, TypedDict, TypeVar, overload from urllib.parse import quote import httpx @@ -202,6 +205,180 @@ REVIEW_POLL_DEFAULT = 10.0 +class NotGiven: + """The type of :data:`NOT_GIVEN`: "this option was not passed", as opposed + to ``None``, which ``Lenz.with_options(timeout=None)`` reads as "no + timeout". Only for type annotations; callers never need to pass it.""" + + _instance: NotGiven | None = None + + def __new__(cls) -> NotGiven: + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __bool__(self) -> Literal[False]: + return False + + def __repr__(self) -> str: + return "NOT_GIVEN" + + def __copy__(self) -> NotGiven: + return self + + def __deepcopy__(self, memo: Any) -> NotGiven: + return self + + +#: The default of ``Lenz.with_options``' parameters: keep what the client has. +NOT_GIVEN: Final = NotGiven() + +#: Headers the request options refuse, in any casing: the SDK sets them +#: itself (``idempotency_key=`` and ``api_key=`` are the way to choose the +#: first two), or httpx computes them. +_RESERVED_HEADERS = frozenset( + { + "x-lenz-api-version", + "idempotency-key", + "authorization", + "content-type", + "content-length", + "host", + "transfer-encoding", + } +) + + +def _check_timeout(value: Any, where: str) -> None: + """A per-request timeout: a finite number of seconds greater than 0, an + ``httpx.Timeout`` (taken as given) or ``None``. ``ValueError`` otherwise, + before any request is made.""" + if value is None or isinstance(value, httpx.Timeout): + return + if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value <= 0: + raise ValueError( + f"{where}: timeout must be a number of seconds greater than 0, None or an httpx.Timeout (got {value!r})." + ) + + +def _check_retries(value: Any, where: str) -> None: + """A retry count: a whole number, 0 or more. ``ValueError`` otherwise.""" + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"{where}: max_retries must be a whole number, 0 or more (got {value!r}).") + + +def _check_headers(value: Any, where: str) -> tuple[tuple[str, str | None], ...]: + """``extra_headers`` as (name, value) pairs in the order given: names + strings outside ``_RESERVED_HEADERS``, values strings or ``None`` + (removes the header a ``with_options`` copy added). ``ValueError`` + otherwise.""" + if value is None: + return () + if not isinstance(value, Mapping): + raise ValueError(f"{where}: extra_headers must be a mapping of header names to strings (got {value!r}).") + pairs: list[tuple[str, str | None]] = [] + for name, header in value.items(): + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"{where}: a header name must be a non-empty string (got {name!r}).") + if name.lower() in _RESERVED_HEADERS: + raise ValueError(f"{where}: the {name} header is set by the SDK and cannot be passed in extra_headers.") + if header is not None and not isinstance(header, str): + raise ValueError(f"{where}: the value of header {name} must be a string or None (got {header!r}).") + pairs.append((name, header)) + return tuple(pairs) + + +def _merge_headers( + lower: Sequence[tuple[str, str]], upper: Sequence[tuple[str, str | None]] +) -> tuple[tuple[str, str], ...]: + """``upper`` over ``lower``, names compared case-insensitively: the last + spelling of a name and its value win, and a ``None`` value removes the + name from ``lower``.""" + merged: dict[str, tuple[str, str]] = {name.lower(): (name, value) for name, value in lower} + for name, value in upper: + if value is None: + merged.pop(name.lower(), None) + else: + merged[name.lower()] = (name, value) + return tuple(merged.values()) + + +@dataclass(frozen=True) +class _CallOptions: + """The request options one call was given (its ``timeout``, + ``max_retries`` and ``extra_headers`` keywords); ``None`` inherits.""" + + timeout: float | httpx.Timeout | None = None + max_retries: int | None = None + headers: tuple[tuple[str, str | None], ...] = () + + +_NO_OPTIONS = _CallOptions() + + +def _call_options( + timeout: float | httpx.Timeout | None, + max_retries: int | None, + extra_headers: Mapping[str, str | None] | None, + where: str, +) -> _CallOptions: + """One call's request options, checked: raises ``ValueError`` before the + call mints a key or makes a request.""" + _check_timeout(timeout, where) + if max_retries is not None: + _check_retries(max_retries, where) + return _CallOptions(timeout=timeout, max_retries=max_retries, headers=_check_headers(extra_headers, where)) + + +@dataclass(frozen=True) +class _ClientOptions: + """What ``Lenz.with_options`` set on a copy, already flattened over the + copy it was made from. ``NOT_GIVEN`` inherits from the ``httpx.Client`` in + use (timeout) or the constructor (retries).""" + + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN + max_retries: int | NotGiven = NOT_GIVEN + headers: tuple[tuple[str, str], ...] = () + + +def _resolve( + call: _CallOptions, + layer: _ClientOptions, + client_timeout: httpx.Timeout, + client_retries: int, + floor: float | None, +) -> tuple[httpx.Timeout | None, int, tuple[tuple[str, str], ...]]: + """The timeout, retry count and option headers of one request. Pure: no + I/O, no clock. + + Per field, the first that is set wins: the call's option, the copy's + (``with_options``), the client's (the ``httpx.Client`` in use for the + timeout, the constructor for retries). Headers merge, the call's over the + copy's. A ``floor`` (``extract`` / ``assess``) applies to an inherited + timeout only, as it always did: when its ``read`` is bounded and shorter + than the floor, the floor replaces the whole timeout; otherwise the + inherited timeout is kept as it is. A timeout of ``None`` means "the + ``httpx.Client``'s own" (httpx's ``USE_CLIENT_DEFAULT``). + """ + timeout: httpx.Timeout | None + if call.timeout is not None: + timeout = httpx.Timeout(call.timeout) + else: + own = None if isinstance(layer.timeout, NotGiven) else httpx.Timeout(layer.timeout) + inherited = client_timeout if own is None else own + if floor is not None and inherited.read is not None and inherited.read < floor: + timeout = httpx.Timeout(floor) + else: + timeout = own + if call.max_retries is not None: + retries = call.max_retries + elif not isinstance(layer.max_retries, NotGiven): + retries = layer.max_retries + else: + retries = client_retries + return timeout, retries, _merge_headers(layer.headers, call.headers) + + def _poll_hint(progress: Any) -> float | None: """The server's suggested wait before the next poll, or None. @@ -262,6 +439,9 @@ def _extracted(body: Any, *, locate: bool | None) -> ExtractedClaims: return out +#: ``Lenz`` or a subclass, for ``with_options``' return type. +_Client = TypeVar("_Client", bound="Lenz") + #: An async job the poll loop waits on: a review or a citation check. _Job = TypeVar("_Job", ReviewFull, Citecheck) @@ -326,11 +506,31 @@ class _VerificationsNamespace: def __init__(self, parent: Lenz) -> None: self._p = parent - def list(self, *, page: int = 1) -> VerificationList: - body = self._p._request("GET", "/verifications", params={"page": page}) + def list( + self, + *, + page: int = 1, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> VerificationList: + """One page of your verifications, newest first. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. + """ + options = _call_options(timeout, max_retries, extra_headers, "verifications.list()") + body = self._p._request("GET", "/verifications", params={"page": page}, options=options) return VerificationList.model_validate(body) - def iter(self, *, page: int = 1) -> Iterator[VerificationListItem]: + def iter( + self, + *, + page: int = 1, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> Iterator[VerificationListItem]: """Every verification on your account, newest first, page after page from ``page`` (default 1). @@ -344,10 +544,25 @@ def iter(self, *, page: int = 1) -> Iterator[VerificationListItem]: print(item.verification_id, item.verdict) Since 3.0. ``list(page=...)`` reads one page. + + Request options (``timeout``, ``max_retries``, ``extra_headers``) + apply to every page request, and a bad one raises here, before the + first page: see :meth:`Lenz.with_options`. """ - return _walk(lambda n: self.list(page=n), _first_page(page)) + _call_options(timeout, max_retries, extra_headers, "verifications.iter()") + return _walk( + lambda n: self.list(page=n, timeout=timeout, max_retries=max_retries, extra_headers=extra_headers), + _first_page(page), + ) - def get(self, verification_id: str) -> Verification: + def get( + self, + verification_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> Verification: """Fetch a single verification by id. Works without an API key — the server accepts optional Bearer: @@ -361,17 +576,29 @@ def get(self, verification_id: str) -> Verification: wait for a run, use ``client.wait(task_id)``. Raises :class:`LenzGoneError` (HTTP 410) when the account's retention period has removed the verification. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + options = _call_options(timeout, max_retries, extra_headers, "verifications.get()") vid = _segment(verification_id, "verifications.get() needs a verification_id.") body = self._p._request( "GET", f"/verifications/{vid}", auth_required=False, auth_optional=True, # send the key if we have one → owner sees private rows + options=options, ) return Verification.model_validate(body) - def get_certificate(self, verification_id: str) -> Certificate: + def get_certificate( + self, + verification_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> Certificate: """Download the warranty certificate for a covered verification. Resolved by (verification, ACCOUNT), not by verification alone: one @@ -388,16 +615,32 @@ def get_certificate(self, verification_id: str) -> Certificate: ``/certificate/.json``, so it verifies with the published open-source checker without involving Lenz. A withdrawn certificate is still served — it is the record of what was warranted. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + options = _call_options(timeout, max_retries, extra_headers, "verifications.get_certificate()") vid = _segment(verification_id, "verifications.get_certificate() needs a verification_id.") - body = self._p._request("GET", f"/verifications/{vid}/certificate") + body = self._p._request("GET", f"/verifications/{vid}/certificate", options=options) return Certificate.model_validate(body) - def delete(self, verification_id: str) -> bool: - """Idempotent. Retry-on-404 returns True ("already deleted").""" + def delete( + self, + verification_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> bool: + """Idempotent. Retry-on-404 returns True ("already deleted"). + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. + """ + options = _call_options(timeout, max_retries, extra_headers, "verifications.delete()") vid = _segment(verification_id, "verifications.delete() needs a verification_id.") try: - self._p._request("DELETE", f"/verifications/{vid}") + self._p._request("DELETE", f"/verifications/{vid}", options=options) return True except LenzError as exc: # Idempotent DELETE: if the row was already gone (e.g. previous @@ -408,7 +651,15 @@ def delete(self, verification_id: str) -> bool: return True raise - def related(self, verification_id: str, *, limit: int = 5) -> RelatedVerifications: + def related( + self, + verification_id: str, + *, + limit: int = 5, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> RelatedVerifications: """Return public verifications semantically related to this one. Server caps ``limit`` to 10. Empty list when the verification has @@ -420,7 +671,11 @@ def related(self, verification_id: str, *, limit: int = 5) -> RelatedVerificatio library item. Raises :class:`LenzGoneError` (HTTP 410) when the account's retention period has removed the verification. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + options = _call_options(timeout, max_retries, extra_headers, "verifications.related()") vid = _segment(verification_id, "verifications.related() needs a verification_id.") body = self._p._request( "GET", @@ -428,6 +683,7 @@ def related(self, verification_id: str, *, limit: int = 5) -> RelatedVerificatio params={"limit": limit}, auth_required=False, auth_optional=True, # send the key if we have one → owner sees own rows + options=options, ) return RelatedVerifications.model_validate(body) @@ -442,13 +698,24 @@ class _AskNamespace: def __init__(self, parent: Lenz) -> None: self._p = parent - def history(self, verification_id: str) -> AskHistory: + def history( + self, + verification_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> AskHistory: """The follow-up conversation on a verification. Raises :class:`LenzGoneError` (HTTP 410) when the account's retention period has removed the verification. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + options = _call_options(timeout, max_retries, extra_headers, "ask.history()") vid = _segment(verification_id, "ask.history() needs a verification_id.") - body = self._p._request("GET", f"/ask/{vid}") + body = self._p._request("GET", f"/ask/{vid}", options=options) return AskHistory.model_validate(body) def send( @@ -459,6 +726,9 @@ def send( language: str = "", idempotency_key: str | None = None, idempotency: bool = True, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> AskReply: """Send a follow-up question on an existing verification. @@ -490,7 +760,11 @@ def send( Paid — see ``client.usage()``. Raises :class:`LenzGoneError` (HTTP 410) when the account's retention period has removed the verification. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + options = _call_options(timeout, max_retries, extra_headers, "ask.send()") vid = _segment(verification_id, "ask.send() needs a verification_id.") payload: dict[str, Any] = {"message": message} if language: @@ -505,12 +779,26 @@ def send( f"/ask/{vid}", json=payload, headers=headers, + options=options, ) return AskReply.model_validate(body) - def reset(self, verification_id: str) -> bool: + def reset( + self, + verification_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> bool: + """Clear the follow-up conversation on a verification. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. + """ + options = _call_options(timeout, max_retries, extra_headers, "ask.reset()") vid = _segment(verification_id, "ask.reset() needs a verification_id.") - self._p._request("DELETE", f"/ask/{vid}") + self._p._request("DELETE", f"/ask/{vid}", options=options) return True @@ -537,6 +825,9 @@ def list( entity: str = "", curated: list[str] | None = None, verdict: str = "", + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> LibraryList: """List the public verification catalog. Works without an API key. @@ -545,7 +836,11 @@ def list( filters by comma-separated labels, e.g. ``"True,False"``. ``sort`` also accepts ``"random"`` alongside ``recent`` / ``most_true`` / ``most_untrue`` / ``relevance``. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + options = _call_options(timeout, max_retries, extra_headers, "library.list()") params: dict[str, Any] = { "page": page, "sort": sort, @@ -563,6 +858,7 @@ def list( "/library", params=params, auth_required=False, + options=options, ) return LibraryList.model_validate(body) @@ -577,6 +873,9 @@ def iter( # ``builtins.list``: in this class body ``list`` is the method above. curated: builtins.list[str] | None = None, verdict: str = "", + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> Iterator[LibraryItem]: """Every item of the public catalog that matches the filters (the ones ``list`` takes), page after page from ``page``. Works without an @@ -589,13 +888,27 @@ def iter( 1 or more, and ``sort="random"`` raises ``ValueError``: a random order is drawn anew for every page, so walking it neither reaches every item nor avoids repeats (use ``list(sort="random")`` for a sample). Since 3.0. + + Request options (``timeout``, ``max_retries``, ``extra_headers``) + apply to every page request, and a bad one raises here, before the + first page: see :meth:`Lenz.with_options`. """ + _call_options(timeout, max_retries, extra_headers, "library.iter()") if sort == "random": raise ValueError('iter cannot walk sort="random" (each page is a fresh sample); call library.list instead.') page = _first_page(page) return _walk( lambda n: self.list( - page=n, sort=sort, search=search, domain=domain, entity=entity, curated=curated, verdict=verdict + page=n, + sort=sort, + search=search, + domain=domain, + entity=entity, + curated=curated, + verdict=verdict, + timeout=timeout, + max_retries=max_retries, + extra_headers=extra_headers, ), page, ) @@ -705,11 +1018,16 @@ def __init__( http_client: httpx.Client | None = None, user_agent: str | None = None, ) -> None: + # The same rule as every request option: refused here, before the + # client exists, rather than failing (or retrying forever) later. + _check_timeout(timeout, "Lenz()") + _check_retries(max_retries, "Lenz()") self._api_key = api_key or os.environ.get("LENZ_API_KEY") or "" self._base_url = (base_url or os.environ.get("LENZ_BASE_URL") or DEFAULT_BASE_URL).rstrip("/") self._timeout = timeout self._max_retries = max_retries self._owns_client = http_client is None + self._options = _ClientOptions() # ``user_agent`` lets a wrapper (e.g. the CLI) override just the UA while # the SDK keeps ownership of every other default header — so a new # default header can't be silently dropped by a hand-copied client. @@ -731,9 +1049,81 @@ def __init__( # ── lifecycle ── def close(self) -> None: + """Close the connection pool, if this client created it. A client + given ``http_client=`` leaves it open (it is yours), and a copy made + by :meth:`with_options` never closes the pool it shares: close the + client you made the copies from.""" if self._owns_client: self._client.close() + def with_options( + self: _Client, + *, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + max_retries: int | NotGiven = NOT_GIVEN, + extra_headers: Mapping[str, str | None] | None = None, + ) -> _Client: + """A copy of this client with other request options, sharing its + connection pool, key and base URL. Cheap: make one per request if you + like. The client it was made from is not changed:: + + fast = client.with_options(timeout=10, max_retries=0) + fast.usage() + + traced = client.with_options(extra_headers={"X-Trace-Id": trace_id}) + traced.assess(claim="...") + + Every method also takes the same three options as keywords, for one + call: ``client.assess(claim="...", timeout=20)``. + + * ``timeout``: one HTTP attempt, in seconds (a number greater than 0) + or an ``httpx.Timeout``. httpx applies it per phase (connect, read, + write, pool) and the read limit to each chunk, so it is an + inactivity limit, not a bound on the whole call, and retries each get + their own. Here ``None`` means no timeout, as on ``Lenz(timeout=None)``; + on a single call ``timeout=None`` keeps the client's (pass + ``httpx.Timeout(None)`` for no timeout on one call). ``extract`` and + ``assess`` take at least 150 s / 100 s when the timeout is inherited + from a copy or the client, as they always did; a timeout passed to the + call itself is used as given, even below that (a shorter one can time + out a call the server is still running: retry it with the same + ``idempotency_key`` to get the answer). + * ``max_retries``: how often a request that failed in a way worth + retrying (a 5xx, a 429, a dropped connection) is sent again: a whole + number, 0 or more. + * ``extra_headers``: headers added to every request, merged over the + copy's own by name, case-insensitively. ``None`` as a value removes a + header a copy added. The SDK's own headers (``X-Lenz-API-Version``, + ``Idempotency-Key``, ``Authorization``, ``Content-Type``, + ``Content-Length``, ``Host``, ``Transfer-Encoding``) are refused. + + Per option, a call's keyword wins over the copy, and the copy over the + client. A bad value raises ``ValueError`` here, before any request. + The wait helpers keep their own ``timeout`` (how long to wait); the + timeout of each of their requests comes from the copy or the client. + + The copy shares the pool: ``close()`` and ``with`` on a copy do + nothing, closing the original closes the pool for every copy, and a + copy is as safe to share across threads as the client. + """ + if not isinstance(timeout, NotGiven): + _check_timeout(timeout, "with_options()") + if not isinstance(max_retries, NotGiven): + _check_retries(max_retries, "with_options()") + headers = _check_headers(extra_headers, "with_options()") + layer = self._options + clone = copy.copy(self) + clone._owns_client = False + clone._options = _ClientOptions( + timeout=layer.timeout if isinstance(timeout, NotGiven) else timeout, + max_retries=layer.max_retries if isinstance(max_retries, NotGiven) else max_retries, + headers=_merge_headers(layer.headers, headers), + ) + clone.verifications = _VerificationsNamespace(clone) + clone.ask = _AskNamespace(clone) + clone.library = _LibraryNamespace(clone) + return clone + def __enter__(self) -> Lenz: return self @@ -754,6 +1144,9 @@ def verify( idempotency_key: str | None = None, source_url: str = "", webhook_url: str = "", + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> TaskAccepted: """Submit a claim for verification. Returns a ``task_id``; the pipeline runs async. For sync ergonomics use ``verify_and_wait``. @@ -788,7 +1181,11 @@ def verify( verification. ``webhook_url`` (optional): where to send this verification's ``verification.*`` events; leave it out (or blank) for your key's default webhook URL. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + _call_options(timeout, max_retries, extra_headers, "verify()") return self._verify_submit( claim=claim, text=text, @@ -798,6 +1195,9 @@ def verify( visibility=visibility, depth=depth, idempotency_key=_call_key(idempotency_key, idempotency), + timeout=timeout, + max_retries=max_retries, + extra_headers=extra_headers, ) def verify_batch( @@ -810,6 +1210,9 @@ def verify_batch( depth: str = "", idempotency_key: str | None = None, idempotency: bool = True, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> BatchAccepted: """Submit multiple claims in one call. Returns a ``batch_id`` and per-claim ``task_id``s. Each item has its own lifecycle and webhook. @@ -834,7 +1237,11 @@ def verify_batch( pin your own with ``idempotency_key=`` (it wins), or pass ``idempotency=False`` to send none. (Since 3.0; 2.x sent a key only when you passed one.) + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + _call_options(timeout, max_retries, extra_headers, "verify_batch()") return self._verify_batch( claims=claims, webhook_url=webhook_url, @@ -842,6 +1249,9 @@ def verify_batch( visibility=visibility, depth=depth, idempotency_key=_call_key(idempotency_key, idempotency), + timeout=timeout, + max_retries=max_retries, + extra_headers=extra_headers, ) def extract( @@ -851,9 +1261,11 @@ def extract( language: str = "", focus: str = "", locate: bool | None = None, - timeout: float | None = None, + timeout: float | httpx.Timeout | None = None, idempotency: bool = True, idempotency_key: str | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> ExtractedClaims: """Pull the verifiable claims out of any text. Sync, free, capped at 1000 calls/account/day (shared across your API keys). @@ -886,10 +1298,12 @@ def extract( unfiltered. Leave it ``None`` to use the server default (currently off); an explicit ``False`` is sent as such. - ``timeout`` (optional): per-call HTTP timeout in seconds, overriding - the client default for this one request. Otherwise ``extract`` uses + ``timeout`` (optional): per-call HTTP timeout in seconds (or an + ``httpx.Timeout``), overriding the client default for this one + request, used as given. Otherwise ``extract`` uses ``EXTRACT_TIMEOUT`` (150s), or your client timeout when you configured a longer one: a long input can take more than a minute to extract. + ``max_retries`` and ``extra_headers``: see :meth:`Lenz.with_options`. ``idempotency`` (default ``True``): send an ``Idempotency-Key`` so the SDK's own retry after a timeout or network drop replays the first @@ -898,6 +1312,7 @@ def extract( retries; pin your own with ``idempotency_key=``, or pass ``idempotency=False`` to send none. Never derived from the text. """ + _call_options(timeout, max_retries, extra_headers, "extract()") return self._extract( text=text, language=language, @@ -905,6 +1320,8 @@ def extract( locate=locate, timeout=timeout, idempotency_key=_call_key(idempotency_key, idempotency), + max_retries=max_retries, + extra_headers=extra_headers, ) def assess( @@ -915,9 +1332,11 @@ def assess( claims: list[str] | None = None, language: str = "", suggest_rewrite: bool = False, - timeout: float | None = None, + timeout: float | httpx.Timeout | None = None, idempotency: bool = True, idempotency_key: str | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> AssessResponse: """Fast verdict via a 3-model frontier panel. Sync, typically ~15s. @@ -978,11 +1397,13 @@ def assess( and is not itself verified: review it, or run it through ``verify``, before using it. ``False`` sends nothing. - ``timeout`` (optional): per-call HTTP timeout in seconds, overriding - the client default for this one request. Both forms otherwise use + ``timeout`` (optional): per-call HTTP timeout in seconds (or an + ``httpx.Timeout``), overriding the client default for this one + request, used as given. Both forms otherwise use ``ASSESS_TIMEOUT`` (100s), or your client timeout when you configured a longer one — the server runs framing and a 3-model panel inside one request, and a long text can use the server's whole 90s budget. + ``max_retries`` and ``extra_headers``: see :meth:`Lenz.with_options`. ``idempotency`` (default ``True``): send an ``Idempotency-Key`` so a retry after a network drop replays the first response instead of @@ -1000,16 +1421,19 @@ def assess( single-string input read as N claims (up to 20) costs N; ``Error`` rows are free. """ + _call_options(timeout, max_retries, extra_headers, "assess()") + if claims is not None and (claim or text): + raise ValueError("assess takes either one claim (claim=) or a list (claims=), not both") key = _call_key(idempotency_key, idempotency) if claims is not None: - if claim or text: - raise ValueError("assess takes either one claim (claim=) or a list (claims=), not both") return self._assess( claims=claims, language=language, suggest_rewrite=suggest_rewrite, timeout=timeout, idempotency_key=key, + max_retries=max_retries, + extra_headers=extra_headers, ) return self._assess( text=claim or text, @@ -1017,6 +1441,8 @@ def assess( suggest_rewrite=suggest_rewrite, timeout=timeout, idempotency_key=key, + max_retries=max_retries, + extra_headers=extra_headers, ) def select( @@ -1027,6 +1453,9 @@ def select( texts: list[str] | None = None, idempotency: bool = True, idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> BatchAccepted: """Resolve a needs-input interrupt by selecting one or more claims. @@ -1046,22 +1475,50 @@ def select( instead of starting a second set. Random per call and reused across this SDK's own retries; pin your own with ``idempotency_key=``, or pass ``idempotency=False`` to send none. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + _call_options(timeout, max_retries, extra_headers, "select()") chosen = claims or texts if not chosen: raise ValueError("select requires a non-empty claims=[...]") - return self._select(task_id, texts=chosen, idempotency_key=_call_key(idempotency_key, idempotency)) + return self._select( + task_id, + texts=chosen, + idempotency_key=_call_key(idempotency_key, idempotency), + timeout=timeout, + max_retries=max_retries, + extra_headers=extra_headers, + ) - def get_status(self, task_id: str) -> TaskStatus: + def get_status( + self, + task_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> TaskStatus: """Poll the pipeline status. Use ``verify_and_wait`` for sync ergonomics. Raises :class:`LenzGoneError` (HTTP 410) when the task completed and the account's retention period has since removed its verification; a task that is still running never answers 410. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ - return self._get_status(task_id) + return self._get_status(task_id, timeout=timeout, max_retries=max_retries, extra_headers=extra_headers) - def cancel(self, task_id: str) -> CancelResult: + def cancel( + self, + task_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> CancelResult: """Stop a verification that has not finished (``POST /verify/{task_id}/cancel``). A cancelled run is not charged and saves nothing; work already under @@ -1092,11 +1549,15 @@ def cancel(self, task_id: str) -> CancelResult: a dropped connection is retried, as for every call that is safe to send twice. + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. + Since 3.0. """ + options = _call_options(timeout, max_retries, extra_headers, "cancel()") tid = _segment(task_id, "cancel() needs a task_id.") path = f"/verify/{tid}/cancel" - body = self._request("POST", path) + body = self._request("POST", path, options=options) if not _is_cancel_body(body, task_id): raise _unexpected_answer("POST", path) return CancelResult.model_validate(body) @@ -1117,6 +1578,8 @@ def verify_and_wait( idempotency: bool = True, idempotency_key: str | None = None, on_progress: Callable[[str, Progress], None] | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> Verification: """Submit + poll until the pipeline terminates. @@ -1146,8 +1609,16 @@ def verify_and_wait( Equivalent to ``wait(verify(claim, ...))``, with the same idempotency-key handling (auto-generate / pin / disable). - ``timeout`` defaults to ``WAIT_TIMEOUT`` (300s). + ``timeout`` defaults to ``WAIT_TIMEOUT`` (300s). The clock starts + once the submit is accepted. + + ``max_retries``: the submit's retries (each poll is one request and a + failed poll is read again on the next round). ``extra_headers``: added + to the submit and to every poll. See :meth:`Lenz.with_options`; the + timeout of each request comes from the copy or the client, since + ``timeout`` here is how long to wait. """ + _call_options(None, max_retries, extra_headers, "verify_and_wait()") key = _call_key(idempotency_key, idempotency) with _carrying_key(key): accepted = self._verify_submit( @@ -1159,9 +1630,14 @@ def verify_and_wait( visibility=visibility, depth=depth, idempotency_key=key, + max_retries=max_retries, + extra_headers=extra_headers, ) logger.info("Submitted task: %s", accepted.task_id) - return self.wait(accepted, timeout=timeout, on_progress=on_progress) + if extra_headers is None: + # Called as in 2.x, so a subclass overriding ``wait`` is still called. + return self.wait(accepted, timeout=timeout, on_progress=on_progress) + return self.wait(accepted, timeout=timeout, on_progress=on_progress, extra_headers=extra_headers) def wait( self, @@ -1169,6 +1645,7 @@ def wait( *, timeout: float = WAIT_TIMEOUT, on_progress: Callable[[str, Progress], None] | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> Verification: """Block until an already-submitted task terminates, then return its ``Verification``. @@ -1190,10 +1667,16 @@ def wait( later poll would answer otherwise. A 5xx, a 429 or a network failure is polled again on the next round. (Since 3.0; 2.x polled through every error until the timeout.) + + ``extra_headers``: added to every poll (see :meth:`Lenz.with_options`). + Each poll is one request, so there is no ``max_retries`` here; its + timeout comes from the copy or the client, capped by what is left of + the wait. """ + options = _call_options(None, None, extra_headers, "wait()") task_id = task if isinstance(task, str) else task.task_id _segment(task_id, "wait() requires a non-empty task_id (got an empty TaskAccepted.task_id).") - terminal, timed_out, stopped = self._poll_to_terminal([task_id], timeout, on_progress) + terminal, timed_out, stopped = self._poll_to_terminal([task_id], timeout, on_progress, options=options) if task_id in stopped: raise stopped[task_id] if task_id in timed_out: @@ -1218,6 +1701,8 @@ def verify_batch_and_wait( timeout: float = WAIT_TIMEOUT, on_progress: Callable[[str, Progress], None] | None = None, idempotency: bool = True, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> list[BatchItemResult]: """Submit a batch and poll every item to a terminal state. @@ -1239,7 +1724,14 @@ def verify_batch_and_wait( ``task_id``. ``idempotency`` / ``idempotency_key``: as on ``verify_batch``. + + ``max_retries``: the submit's retries (each poll is one request and a + failed poll is read again on the next round). ``extra_headers``: added + to the submit and to every poll. See :meth:`Lenz.with_options`; the + timeout of each request comes from the copy or the client, since + ``timeout`` here is how long to wait. """ + options = _call_options(None, max_retries, extra_headers, "verify_batch_and_wait()") key = _call_key(idempotency_key, idempotency) with _carrying_key(key): accepted = self._verify_batch( @@ -1249,9 +1741,13 @@ def verify_batch_and_wait( visibility=visibility, depth=depth, idempotency_key=key, + max_retries=max_retries, + extra_headers=extra_headers, ) ids = [it.task_id for it in accepted.items if it.task_id] - terminal, timed_out, stopped = self._poll_to_terminal(ids, timeout, on_progress) + terminal, timed_out, stopped = self._poll_to_terminal( + ids, timeout, on_progress, options=_CallOptions(headers=options.headers) + ) results: list[BatchItemResult] = [] for it in accepted.items: # preserve input order @@ -1303,6 +1799,9 @@ def review( webhook_url: str | None = None, visibility: str = "private", idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> ReviewStarted: """Start a review of a draft. Returns at once with a ``review_id``; the review takes two to four minutes. Use ``review_and_wait`` to @@ -1355,7 +1854,11 @@ def review( submit cannot start a second review; a resend with the same key within 24 hours returns the same review, and a new key is a new review. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + options = _call_options(timeout, max_retries, extra_headers, "review()") if not text or not text.strip(): raise ValueError("review() needs the draft text, or one public http(s) URL.") payload: dict[str, Any] = {"text": text} @@ -1391,7 +1894,12 @@ def review( headers = {"Idempotency-Key": idempotency_key or uuid.uuid4().hex} try: body = self._request( - "POST", "/review", json=payload, headers=headers, conflict_settles=_names_the_job("review_id") + "POST", + "/review", + json=payload, + headers=headers, + conflict_settles=_names_the_job("review_id"), + options=options, ) except LenzError as exc: # A retried submit (same key) that lands while the first attempt's @@ -1411,31 +1919,73 @@ def review( return ReviewStarted.model_validate(body) @overload - def get_review(self, review_id: str) -> ReviewFull: ... + def get_review( + self, + review_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> ReviewFull: ... @overload - def get_review(self, review_id: str, *, view: Literal["full"]) -> ReviewFull: ... + def get_review( + self, + review_id: str, + *, + view: Literal["full"], + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> ReviewFull: ... @overload - def get_review(self, review_id: str, *, view: Literal["issues"]) -> ReviewIssues: ... + def get_review( + self, + review_id: str, + *, + view: Literal["issues"], + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> ReviewIssues: ... - def get_review(self, review_id: str, *, view: str = "full") -> ReviewFull | ReviewIssues: + def get_review( + self, + review_id: str, + *, + view: str = "full", + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> ReviewFull | ReviewIssues: """Read a review. ``view="issues"`` returns just the issues and the failures (``ReviewIssues``, no ``claims``); the default returns every claim (``ReviewFull``). Raises :class:`LenzGoneError` (410) once the account's retention period has removed the review. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + options = _call_options(timeout, max_retries, extra_headers, "get_review()") rid = _segment(review_id, "get_review() needs a review_id.") if view == "issues": - body = self._request("GET", f"/reviews/{rid}", params={"view": "issues"}) + body = self._request("GET", f"/reviews/{rid}", params={"view": "issues"}, options=options) return ReviewIssues.model_validate(body) if view != "full": raise ValueError(f"view must be 'full' or 'issues' (got {view!r}).") - return ReviewFull.model_validate(self._request("GET", f"/reviews/{rid}")) + return ReviewFull.model_validate(self._request("GET", f"/reviews/{rid}", options=options)) - def cancel_review(self, review_id: str) -> ReviewFull: + def cancel_review( + self, + review_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> ReviewFull: """Stop a review (``POST /reviews/{review_id}/cancel``), including the deep checks it started. Returns the review as it stands afterwards, the full view: ``status`` is ``"cancelled"``. @@ -1449,11 +1999,15 @@ def cancel_review(self, review_id: str) -> ReviewFull: another account's, and :class:`LenzGoneError` (410) once the account's retention period has removed it. + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. + Since 3.0. """ + options = _call_options(timeout, max_retries, extra_headers, "cancel_review()") rid = _segment(review_id, "cancel_review() needs a review_id.") path = f"/reviews/{rid}/cancel" - body = self._request("POST", path) + body = self._request("POST", path, options=options) if not _is_full_review_body(body, review_id): raise _unexpected_answer("POST", path) return ReviewFull.model_validate(body) @@ -1475,6 +2029,8 @@ def review_and_wait( webhook_url: str | None = None, visibility: str = "private", idempotency_key: str | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> ReviewFull: """Start a review (``review(text, ...)``, which documents every option but ``timeout`` and ``on_update``) and poll it until it ends. @@ -1489,8 +2045,16 @@ def review_and_wait( Raises :class:`ReviewFailed` when the review ends ``failed`` (its ``failure`` says why), and :class:`ReviewTimeout` when ``timeout`` seconds pass first: the review keeps running, and the - error carries its ``review_id`` and the last body read. + error carries its ``review_id`` and the last body read. The clock + starts once the submit is accepted. + + ``max_retries``: the submit's retries (each poll is one request and a + failed poll is read again on the next round). ``extra_headers``: added + to the submit and to every poll. See :meth:`Lenz.with_options`; the + timeout of each request comes from the copy or the client, since + ``timeout`` here is how long to wait. """ + _call_options(None, max_retries, extra_headers, "review_and_wait()") # The key is minted here (as ``review`` would) so the wait's errors carry it too. key = idempotency_key or uuid.uuid4().hex with _carrying_key(key): @@ -1507,9 +2071,13 @@ def review_and_wait( webhook_url=webhook_url, visibility=visibility, idempotency_key=key, + max_retries=max_retries, + extra_headers=extra_headers, ) logger.info("Submitted review: %s", started.review_id) - return self._wait_review(started.review_id, timeout=timeout, on_update=on_update) + return self._wait_review( + started.review_id, timeout=timeout, on_update=on_update, extra_headers=extra_headers + ) # ── /citecheck: the citation check on its own ── @@ -1522,6 +2090,9 @@ def citecheck( language: str = "", webhook_url: str | None = None, idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> CitecheckStarted: """Start a citation check. Returns at once with a ``citecheck_id``; use ``citecheck_and_wait`` to block until it ends, or @@ -1546,7 +2117,11 @@ def citecheck( default webhook URL, ``""`` sends none, a URL sends them there. An ``Idempotency-Key`` is generated when you pass none: a resend with the same key within 24 hours returns the same check. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. """ + options = _call_options(timeout, max_retries, extra_headers, "citecheck()") has_text = bool(text and text.strip()) if has_text == (pairs is not None): raise ValueError("citecheck() needs exactly one of text and pairs.") @@ -1562,7 +2137,12 @@ def citecheck( headers = {"Idempotency-Key": idempotency_key or uuid.uuid4().hex} try: body = self._request( - "POST", "/citecheck", json=payload, headers=headers, conflict_settles=_names_the_job("citecheck_id") + "POST", + "/citecheck", + json=payload, + headers=headers, + conflict_settles=_names_the_job("citecheck_id"), + options=options, ) except LenzError as exc: # A retried submit (same key) that lands while the first attempt's @@ -1576,13 +2156,32 @@ def citecheck( with _carrying_key(headers["Idempotency-Key"], unreadable=True): return CitecheckStarted.model_validate(body) - def get_citecheck(self, citecheck_id: str) -> Citecheck: + def get_citecheck( + self, + citecheck_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> Citecheck: """Read a citation check. Raises :class:`LenzGoneError` (410) once - the account's retention period has removed it.""" + the account's retention period has removed it. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. + """ + options = _call_options(timeout, max_retries, extra_headers, "get_citecheck()") cid = _segment(citecheck_id, "get_citecheck() needs a citecheck_id.") - return Citecheck.model_validate(self._request("GET", f"/citechecks/{cid}")) + return Citecheck.model_validate(self._request("GET", f"/citechecks/{cid}", options=options)) - def cancel_citecheck(self, citecheck_id: str) -> Citecheck: + def cancel_citecheck( + self, + citecheck_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> Citecheck: """Stop a citation check (``POST /citechecks/{citecheck_id}/cancel``). Returns the check as it stands afterwards: ``status`` is ``"cancelled"``. @@ -1596,11 +2195,15 @@ def cancel_citecheck(self, citecheck_id: str) -> Citecheck: account's, or the id of a review, and :class:`LenzGoneError` (410) once the account's retention period has removed it. + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. + Since 3.0. """ + options = _call_options(timeout, max_retries, extra_headers, "cancel_citecheck()") cid = _segment(citecheck_id, "cancel_citecheck() needs a citecheck_id.") path = f"/citechecks/{cid}/cancel" - body = self._request("POST", path) + body = self._request("POST", path, options=options) if not _is_citecheck_body(body, citecheck_id): raise _unexpected_answer("POST", path) return Citecheck.model_validate(body) @@ -1616,6 +2219,8 @@ def citecheck_and_wait( language: str = "", webhook_url: str | None = None, idempotency_key: str | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> Citecheck: """Start a citation check (``citecheck(text, ...)``, which documents every option but ``timeout`` and ``on_update``) and poll it until @@ -1627,8 +2232,16 @@ def citecheck_and_wait( changed. Raises :class:`CitecheckFailed` when the check ends ``failed``, and :class:`CitecheckTimeout` when ``timeout`` seconds pass first: the check keeps running, and the error carries its - ``citecheck_id`` and the last body read. + ``citecheck_id`` and the last body read. The clock starts once the + submit is accepted. + + ``max_retries``: the submit's retries (each poll is one request and a + failed poll is read again on the next round). ``extra_headers``: added + to the submit and to every poll. See :meth:`Lenz.with_options`; the + timeout of each request comes from the copy or the client, since + ``timeout`` here is how long to wait. """ + _call_options(None, max_retries, extra_headers, "citecheck_and_wait()") # The key is minted here (as ``citecheck`` would) so the wait's errors carry it too. key = idempotency_key or uuid.uuid4().hex with _carrying_key(key): @@ -1639,9 +2252,13 @@ def citecheck_and_wait( language=language, webhook_url=webhook_url, idempotency_key=key, + max_retries=max_retries, + extra_headers=extra_headers, ) logger.info("Submitted citation check: %s", started.citecheck_id) - return self._wait_citecheck(started.citecheck_id, timeout=timeout, on_update=on_update) + return self._wait_citecheck( + started.citecheck_id, timeout=timeout, on_update=on_update, extra_headers=extra_headers + ) def _wait_citecheck( self, @@ -1649,6 +2266,7 @@ def _wait_citecheck( *, timeout: float, on_update: Callable[[Citecheck], None] | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> Citecheck: """The poll loop behind ``citecheck_and_wait`` (and ``lenz citecheck --resume``).""" @@ -1669,6 +2287,7 @@ def timed_out(last: Citecheck | None) -> Exception: parse=lambda body: Citecheck.model_validate(body) if _is_citecheck_body(body, citecheck_id) else None, failed=_citecheck_failed, timed_out=timed_out, + options=_call_options(None, None, extra_headers, "citecheck_and_wait()"), ) def _wait_review( @@ -1677,6 +2296,7 @@ def _wait_review( *, timeout: float, on_update: Callable[[ReviewFull], None] | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> ReviewFull: """The poll loop behind ``review_and_wait`` (and ``lenz review --resume``).""" @@ -1697,6 +2317,7 @@ def timed_out(last: ReviewFull | None) -> Exception: parse=lambda body: ReviewFull.model_validate(body) if _is_full_review_body(body, review_id) else None, failed=_review_failed, timed_out=timed_out, + options=_call_options(None, None, extra_headers, "review_and_wait()"), ) def _wait_job( @@ -1708,6 +2329,7 @@ def _wait_job( parse: Callable[[Any], _Job | None], failed: Callable[[_Job], Exception], timed_out: Callable[[_Job | None], Exception], + options: _CallOptions = _NO_OPTIONS, ) -> _Job: """The poll loop behind every ``*_and_wait`` of an async job (a review, a citation check). @@ -1736,7 +2358,7 @@ def _wait_job( raise timed_out(last) first = False try: - body = self._request("GET", path, max_retries=0, timeout=self._poll_timeout(remaining)) + body = self._request("GET", path, max_retries=0, timeout=self._poll_timeout(remaining), options=options) job = parse(body) if job is None: raise ValueError("not this job's body") @@ -1781,6 +2403,8 @@ def _poll_to_terminal( task_ids: list[str], timeout: float, on_progress: Callable[[str, Progress], None] | None = None, + *, + options: _CallOptions = _NO_OPTIONS, ) -> tuple[dict[str, TaskStatus], set[str], dict[str, LenzError]]: """Round-robin poll ``task_ids`` until each reaches a terminal state (completed / needs_input / failed / cancelled) or the deadline elapses. @@ -1846,6 +2470,7 @@ def _poll_to_terminal( f"/verify/status/{quote(task_id, safe='')}", max_retries=0, timeout=self._poll_timeout(remaining), + options=options, ) status = TaskStatus.model_validate(body) except LenzAuthError: @@ -1951,8 +2576,20 @@ def _verification_from_terminal(self, status: TaskStatus, task_id: str) -> Verif # ── account ── - def usage(self) -> Usage: - body = self._request("GET", "/me/usage") + def usage( + self, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> Usage: + """The account's plan, credits and per-capability usage. + + Request options (``timeout``, ``max_retries``, ``extra_headers``): + see :meth:`Lenz.with_options`. + """ + options = _call_options(timeout, max_retries, extra_headers, "usage()") + body = self._request("GET", "/me/usage", options=options) return Usage.model_validate(body) # ── verb-level submit helpers (used by the verify namespace) ── @@ -1968,7 +2605,11 @@ def _verify_submit( visibility: str = "", depth: str = "", idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> TaskAccepted: + options = _call_options(timeout, max_retries, extra_headers, "verify()") payload: dict[str, Any] = { "text": claim or text, "source_url": source_url, @@ -1992,7 +2633,7 @@ def _verify_submit( if idempotency_key: headers["Idempotency-Key"] = idempotency_key with _carrying_key(idempotency_key, unreadable=True): - body = self._request("POST", "/verify", json=payload, headers=headers) + body = self._request("POST", "/verify", json=payload, headers=headers, options=options) return TaskAccepted.model_validate(body) def _verify_batch( @@ -2004,7 +2645,11 @@ def _verify_batch( visibility: str = "", depth: str = "", idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> BatchAccepted: + options = _call_options(timeout, max_retries, extra_headers, "verify_batch()") # ``webhook_url`` and ``language`` are batch-wide defaults; any # per-item value on a claim dict overrides them server-side. # Per-item items are validated as plain dicts at runtime — the @@ -2025,35 +2670,22 @@ def _verify_batch( if idempotency_key: headers["Idempotency-Key"] = idempotency_key with _carrying_key(idempotency_key, unreadable=True): - body = self._request("POST", "/verify/batch", json=payload, headers=headers) + body = self._request("POST", "/verify/batch", json=payload, headers=headers, options=options) return BatchAccepted.model_validate(body) - def _timeout_at_least(self, floor: float) -> float | None: - """``floor`` seconds, or ``None`` (the client's own timeout) when that - is already at least as long, or unbounded. - - Never a flat assignment: a caller who configured a longer timeout asked - for it, and shortening it would be this SDK quietly overruling them. It - reads the client actually in use, so an ``httpx.Client`` passed as - ``http_client=`` keeps its own setting too. - """ - current = self._client.timeout.read - if current is None or current >= floor: - return None - return floor - def _poll_timeout(self, remaining: float) -> httpx.Timeout | None: """The timeouts of one poll request: each phase (connect, read, write, - pool) the client in use configured, capped by what is left of the + pool) the copy or the client in use configured, capped by what is left of the wait's deadline (an unbounded phase gets just that). Past the deadline - (only a ``timeout <= 0`` wait polls then) ``None``: the client's own. + (only a ``timeout <= 0`` wait polls then) ``None``: the copy's or the client's own. Reads the client actually in use, so ``Lenz(timeout=None)``, an ``httpx.Timeout`` and an ``httpx.Client`` passed as ``http_client=`` - all work.""" + all work, and so does a copy's timeout (``with_options``).""" if remaining <= 0: return None - own = self._client.timeout + layer = self._options.timeout + own = self._client.timeout if isinstance(layer, NotGiven) else httpx.Timeout(layer) def cap(phase: float | None) -> float: return remaining if phase is None else min(phase, remaining) @@ -2067,9 +2699,12 @@ def _extract( language: str = "", focus: str = "", locate: bool | None = None, - timeout: float | None = None, + timeout: float | httpx.Timeout | None = None, idempotency_key: str | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> ExtractedClaims: + options = _call_options(timeout, max_retries, extra_headers, "extract()") payload: dict[str, Any] = {"text": text} if language: payload["language"] = language @@ -2081,13 +2716,13 @@ def _extract( # omitted value leaves the server default in charge. if locate is not None: payload["locate"] = locate - if timeout is None: - timeout = self._timeout_at_least(EXTRACT_TIMEOUT) headers = {} if idempotency_key: headers["Idempotency-Key"] = idempotency_key with _carrying_key(idempotency_key, unreadable=True): - body = self._request("POST", "/extract", json=payload, timeout=timeout, headers=headers) + body = self._request( + "POST", "/extract", json=payload, headers=headers, options=options, floor=EXTRACT_TIMEOUT + ) return _extracted(body, locate=locate) def _assess( @@ -2097,9 +2732,12 @@ def _assess( claims: list[str] | None = None, language: str = "", suggest_rewrite: bool = False, - timeout: float | None = None, + timeout: float | httpx.Timeout | None = None, idempotency_key: str | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, ) -> AssessResponse: + options = _call_options(timeout, max_retries, extra_headers, "assess()") # The single form keeps its historical wire body (`text`). The list # form sends `claims` and never `text` — the server rejects a body # carrying both with a 422. No client-side count / length checks on @@ -2116,27 +2754,47 @@ def _assess( # idempotency body) is exactly what it was before. if suggest_rewrite: payload["suggest_rewrite"] = True - if timeout is None: - timeout = self._timeout_at_least(ASSESS_TIMEOUT) headers = {} if idempotency_key: headers["Idempotency-Key"] = idempotency_key with _carrying_key(idempotency_key, unreadable=True): - body = self._request("POST", "/assess", json=payload, timeout=timeout, headers=headers) + body = self._request( + "POST", "/assess", json=payload, headers=headers, options=options, floor=ASSESS_TIMEOUT + ) return AssessResponse.model_validate(body) - def _select(self, task_id: str, *, texts: list[str], idempotency_key: str | None = None) -> BatchAccepted: + def _select( + self, + task_id: str, + *, + texts: list[str], + idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> BatchAccepted: + options = _call_options(timeout, max_retries, extra_headers, "select()") tid = _segment(task_id, "select() needs a task_id.") headers = {} if idempotency_key: headers["Idempotency-Key"] = idempotency_key with _carrying_key(idempotency_key, unreadable=True): - body = self._request("POST", f"/verify/{tid}/select", json={"texts": texts}, headers=headers) + body = self._request( + "POST", f"/verify/{tid}/select", json={"texts": texts}, headers=headers, options=options + ) return BatchAccepted.model_validate(body) - def _get_status(self, task_id: str) -> TaskStatus: + def _get_status( + self, + task_id: str, + *, + timeout: float | httpx.Timeout | None = None, + max_retries: int | None = None, + extra_headers: Mapping[str, str | None] | None = None, + ) -> TaskStatus: + options = _call_options(timeout, max_retries, extra_headers, "get_status()") tid = _segment(task_id, "get_status() needs a task_id.") - body = self._request("GET", f"/verify/status/{tid}") + body = self._request("GET", f"/verify/status/{tid}", options=options) return TaskStatus.model_validate(body) # ── HTTP plumbing ── @@ -2154,10 +2812,24 @@ def _request( timeout: float | httpx.Timeout | None = None, max_retries: int | None = None, conflict_settles: Callable[[Any], bool] | None = None, + options: _CallOptions = _NO_OPTIONS, + floor: float | None = None, ) -> dict[str, Any]: """One API call, with the retry ladder. Every ``LenzError`` it raises - carries the ``Idempotency-Key`` it sent (``exc.idempotency_key``).""" + carries the ``Idempotency-Key`` it sent (``exc.idempotency_key``). + + ``options`` are the call's request options and ``floor`` the minimum + an inherited timeout gets (``_resolve``). ``timeout`` and + ``max_retries`` are the SDK's own settings for one request (a wait's + polls): when set they win over every option.""" key = (headers or {}).get("Idempotency-Key") or None + resolved, retries, option_headers = _resolve( + options, self._options, self._client.timeout, self._max_retries, floor + ) + if timeout is not None: + resolved = httpx.Timeout(timeout) + if max_retries is not None: + retries = max_retries with _carrying_key(key, unreadable=True): return self._send( method, @@ -2165,10 +2837,11 @@ def _request( json=json, params=params, headers=headers, + option_headers=option_headers, auth_required=auth_required, auth_optional=auth_optional, - timeout=timeout, - max_retries=max_retries, + timeout=resolved, + retries=retries, conflict_settles=conflict_settles, ) @@ -2180,15 +2853,13 @@ def _send( json: dict[str, Any] | None, params: dict[str, Any] | None, headers: dict[str, str] | None, + option_headers: Sequence[tuple[str, str]], auth_required: bool, auth_optional: bool, - timeout: float | httpx.Timeout | None, - max_retries: int | None, + timeout: httpx.Timeout | None, + retries: int, conflict_settles: Callable[[Any], bool] | None, ) -> dict[str, Any]: - # ``max_retries`` overrides the client's retry count for this request - # only (the review wait polls with 0 and does its own pacing). - retries = self._max_retries if max_retries is None else max_retries if auth_required and not self._api_key: raise LenzAuthError( message="API key required", @@ -2202,6 +2873,14 @@ def _send( url = f"{self._base_url}{path}" req_headers = dict(headers or {}) + # The request options' headers, over the method's own (case-insensitive; + # none is added when no option was given). httpx then puts the + # ``httpx.Client``'s own headers under them, so an option header also + # replaces a default one like ``User-Agent``. + for name, value in option_headers: + for same in [k for k in req_headers if k.lower() == name.lower()]: + del req_headers[same] + req_headers[name] = value # Attach the bearer on authed endpoints AND on opt-in optional-auth ones # (`auth_optional=True`). The server returns a caller's own private/hidden # rows only to the owning bearer, so `verifications.get` must send a key it @@ -2214,9 +2893,8 @@ def _send( # The version is part of the request, not of the HTTP client: a client # passed as ``http_client=`` may carry no version header or a stale one. req_headers[_VERSION_HEADER] = API_VERSION - # A per-call ``timeout`` overrides the client-wide one for this request - # only; ``None`` keeps httpx on the client default. - req_timeout = httpx.USE_CLIENT_DEFAULT if timeout is None else httpx.Timeout(timeout) + # ``None``: the ``httpx.Client``'s own timeout. + req_timeout = httpx.USE_CLIENT_DEFAULT if timeout is None else timeout last_exc: Exception | None = None for attempt in range(retries + 1): @@ -2526,4 +3204,4 @@ def _retry_sleep(attempt: int) -> float: return RETRY_BACKOFF[-1] -__all__ = ["API_VERSION", "DEFAULT_BASE_URL", "Lenz", "VerifyBatchItem"] +__all__ = ["API_VERSION", "DEFAULT_BASE_URL", "NOT_GIVEN", "Lenz", "NotGiven", "VerifyBatchItem"] diff --git a/tests/test_public_call_bodies.py b/tests/test_public_call_bodies.py index fbc09fb..d15e06d 100644 --- a/tests/test_public_call_bodies.py +++ b/tests/test_public_call_bodies.py @@ -414,8 +414,13 @@ def test_no_body_no_key(self, client: Lenz, method: str, path: str, fixture: str request = route.calls.last.request assert request.content == b"" assert "Idempotency-Key" not in request.headers - # The id is the only parameter: no option can reach the wire. - assert list(inspect.signature(getattr(Lenz, method)).parameters) == ["self", _ID[method]] + # The id is the only parameter besides the request options, which are + # keyword-only and reach the wire only as the headers asked for. + params = inspect.signature(getattr(Lenz, method)).parameters + assert list(params) == ["self", _ID[method], "timeout", "max_retries", "extra_headers"] + assert all( + params[n].kind is inspect.Parameter.KEYWORD_ONLY for n in ("timeout", "max_retries", "extra_headers") + ) # ── 3.0: every forwarded option is a named, keyword-only parameter ───────── diff --git a/tests/test_request_options.py b/tests/test_request_options.py new file mode 100644 index 0000000..71ca720 --- /dev/null +++ b/tests/test_request_options.py @@ -0,0 +1,778 @@ +"""Request options: ``timeout``, ``max_retries`` and ``extra_headers`` on every +public method, and ``Lenz.with_options``. + +* The parity map: every public method and the options it takes, by name. +* Forwarding: a method's options reach every request it makes (submit, + polls, pages, retries). +* Same bytes: with options set, the URL and body are those sent without them, + and the headers differ only by the ones asked for. +* Precedence: call over copy over client, the extract / assess floors on an + inherited timeout only, ``None`` vs ``NOT_GIVEN``. +* Headers, ownership of the shared pool, and validation. +""" + +from __future__ import annotations + +import inspect +import math +import sys +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from typing import Any + +import httpx +import pytest +from freeze_cases import ( + ACCEPTED, + ASK_HISTORY, + ASK_REPLY, + ASSESSED, + BATCH, + CANCEL_CHECK, + CANCEL_REVIEW, + CANCEL_TASK, + CERT, + CHECK_DONE, + CID, + DETAIL, + DONE, + DRAFT, + EXTRACTED, + LIB_1, + LIB_2, + PAGE_1, + PAGE_2, + RELATED, + REVIEW_DONE, + REVIEW_RUNNING, + RID, + RUNNING, + RUNNING_NO_HINT, + S503, + SELECTED, + TASK, + USAGE, +) +from freeze_harness import API_KEY, BASE, PINNED, Recording, recording + +import lenz_io +from lenz_io import NOT_GIVEN, Lenz, NotGiven +from lenz_io.client import EXTRACT_TIMEOUT, WAIT_TIMEOUT + +MARK = "X-Marker" + + +@pytest.fixture(autouse=True) +def _no_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("LENZ_API_KEY", raising=False) + monkeypatch.delenv("LENZ_BASE_URL", raising=False) + + +@contextmanager +def recorded(answers: dict[tuple[str, str], list[Any]]) -> Iterator[Recording]: + """Record the requests of the block, under a fake clock patched for the + block only (so a test's own monkeypatches are left alone).""" + with pytest.MonkeyPatch.context() as mp, recording(mp, answers) as rec: + yield rec + + +def _headers(request: dict[str, Any]) -> dict[str, str]: + return {name.lower(): value for name, value in request["headers"]} + + +# ── the parity map ───────────────────────────────────────────────────────── +# +# Every public method, the Node SDK's name for it, and the request options it +# takes. ``call``: all three, ``timeout`` = one HTTP attempt (``None`` = +# inherit). ``wait``: ``timeout`` is the wait budget and only ``extra_headers`` +# is new (each poll is one request, so no ``max_retries``). ``submit_wait``: +# ``timeout`` is the wait budget, ``max_retries`` the submit's. +CALL = {"timeout", "max_retries", "extra_headers"} +PARITY: dict[str, tuple[str, str]] = { + "verify": ("verify", "call"), + "verify_batch": ("verifyBatch", "call"), + "extract": ("extract", "call"), + "assess": ("assess", "call"), + "select": ("select", "call"), + "get_status": ("getStatus", "call"), + "cancel": ("cancel", "call"), + "review": ("review", "call"), + "get_review": ("getReview", "call"), + "cancel_review": ("cancelReview", "call"), + "citecheck": ("citecheck", "call"), + "get_citecheck": ("getCitecheck", "call"), + "cancel_citecheck": ("cancelCitecheck", "call"), + "usage": ("usage", "call"), + "wait": ("wait", "wait"), + "verify_and_wait": ("verifyAndWait", "submit_wait"), + "verify_batch_and_wait": ("verifyBatchAndWait", "submit_wait"), + "review_and_wait": ("reviewAndWait", "submit_wait"), + "citecheck_and_wait": ("citecheckAndWait", "submit_wait"), + "verifications.list": ("verifications.list", "call"), + "verifications.iter": ("verifications.listAll", "call"), + "verifications.get": ("verifications.get", "call"), + "verifications.get_certificate": ("verifications.getCertificate", "call"), + "verifications.delete": ("verifications.delete", "call"), + "verifications.related": ("verifications.related", "call"), + "ask.history": ("ask.history", "call"), + "ask.send": ("ask.send", "call"), + "ask.reset": ("ask.reset", "call"), + "library.list": ("library.list", "call"), + "library.iter": ("library.listAll", "call"), + "with_options": ("withOptions", "with_options"), + "close": ("(none)", "lifecycle"), +} + + +def _public_methods() -> dict[str, Callable[..., Any]]: + client = Lenz(api_key=API_KEY) + out: dict[str, Callable[..., Any]] = {} + for name, value in inspect.getmembers(client): + if name.startswith("_"): + continue + if name in ("verifications", "ask", "library"): + for sub, method in inspect.getmembers(value): + if not sub.startswith("_") and callable(method): + out[f"{name}.{sub}"] = method + elif callable(value): + out[name] = value + client.close() + return out + + +def test_every_public_method_is_in_the_parity_map() -> None: + assert sorted(_public_methods()) == sorted(PARITY) + + +@pytest.mark.parametrize("name", sorted(PARITY)) +def test_each_method_takes_the_options_the_map_says(name: str) -> None: + kind = PARITY[name][1] + params = inspect.signature(_public_methods()[name]).parameters + options = {n: p for n, p in params.items() if n in CALL} + if kind == "lifecycle": + assert options == {} + return + for p in options.values(): + assert p.kind is inspect.Parameter.KEYWORD_ONLY, (name, p.name) + if kind == "call": + assert set(options) == CALL + assert all(p.default is None for p in options.values()) + elif kind == "wait": + assert set(options) == {"timeout", "extra_headers"} + assert options["timeout"].default == WAIT_TIMEOUT # the wait budget, unchanged + assert options["extra_headers"].default is None + elif kind == "submit_wait": + assert set(options) == CALL + assert isinstance(options["timeout"].default, float) # the wait budget, unchanged + assert options["max_retries"].default is None and options["extra_headers"].default is None + else: + assert kind == "with_options" + assert set(options) == CALL + assert options["timeout"].default is NOT_GIVEN + assert options["max_retries"].default is NOT_GIVEN + assert options["extra_headers"].default is None + + +@pytest.mark.skipif(sys.version_info < (3, 11), reason="typing.get_overloads is 3.11+") +def test_get_review_overloads_all_take_the_options() -> None: + from typing import get_overloads # type: ignore[attr-defined,unused-ignore] + + overloads = get_overloads(Lenz.get_review) + assert len(overloads) == 3 + for fn in overloads: + assert CALL <= set(inspect.signature(fn).parameters) + + +# ── every method, run with and without options ───────────────────────────── + +Answers = dict[tuple[str, str], list[Any]] +Run = Callable[[Lenz, dict[str, Any]], Any] + +# Each method: how to call it, its scripted answers (a 503 first wherever the +# call retries, so a retry is a recorded request too), and its kind. +CALLS: dict[str, tuple[Run, Answers]] = { + "verify": (lambda c, o: c.verify("A.", idempotency_key=PINNED, **o), {("POST", "/verify"): [S503, ACCEPTED]}), + "verify_batch": ( + lambda c, o: c.verify_batch(claims=[{"claim": "A."}], idempotency_key=PINNED, **o), + {("POST", "/verify/batch"): [S503, BATCH]}, + ), + "extract": ( + lambda c, o: c.extract(text="Doc.", idempotency_key=PINNED, **o), + {("POST", "/extract"): [S503, EXTRACTED]}, + ), + "assess": (lambda c, o: c.assess("A.", idempotency_key=PINNED, **o), {("POST", "/assess"): [S503, ASSESSED]}), + "select": ( + lambda c, o: c.select(TASK, claims=["A."], idempotency_key=PINNED, **o), + {("POST", f"/verify/{TASK}/select"): [S503, SELECTED]}, + ), + "get_status": (lambda c, o: c.get_status(TASK, **o), {("GET", f"/verify/status/{TASK}"): [S503, DONE]}), + "cancel": ( + lambda c, o: c.cancel(CANCEL_TASK["task_id"], **o), + {("POST", f"/verify/{CANCEL_TASK['task_id']}/cancel"): [S503, (200, CANCEL_TASK)]}, + ), + "review": ( + lambda c, o: c.review(DRAFT, idempotency_key=PINNED, **o), + {("POST", "/review"): [S503, (202, {"review_id": RID, "status": "queued"})]}, + ), + "get_review": ( + lambda c, o: c.get_review(RID, view="issues", **o), + {("GET", f"/reviews/{RID}"): [S503, (200, REVIEW_DONE)]}, + ), + "cancel_review": ( + lambda c, o: c.cancel_review(CANCEL_REVIEW["review_id"], **o), + {("POST", f"/reviews/{CANCEL_REVIEW['review_id']}/cancel"): [S503, (200, CANCEL_REVIEW)]}, + ), + "citecheck": ( + lambda c, o: c.citecheck(DRAFT, idempotency_key=PINNED, **o), + {("POST", "/citecheck"): [S503, (202, {"citecheck_id": CID, "status": "queued"})]}, + ), + "get_citecheck": ( + lambda c, o: c.get_citecheck(CID, **o), + {("GET", f"/citechecks/{CID}"): [S503, (200, CHECK_DONE)]}, + ), + "cancel_citecheck": ( + lambda c, o: c.cancel_citecheck(CID, **o), + {("POST", f"/citechecks/{CID}/cancel"): [S503, (200, CANCEL_CHECK)]}, + ), + "usage": (lambda c, o: c.usage(**o), {("GET", "/me/usage"): [S503, USAGE]}), + "wait": (lambda c, o: c.wait(TASK, **o), {("GET", f"/verify/status/{TASK}"): [S503, RUNNING, DONE]}), + "verify_and_wait": ( + lambda c, o: c.verify_and_wait("A.", idempotency_key=PINNED, **o), + {("POST", "/verify"): [S503, ACCEPTED], ("GET", f"/verify/status/{TASK}"): [S503, RUNNING, DONE]}, + ), + "verify_batch_and_wait": ( + lambda c, o: c.verify_batch_and_wait(claims=[{"claim": "A."}, {"claim": "B."}], idempotency_key=PINNED, **o), + { + ("POST", "/verify/batch"): [S503, BATCH], + ("GET", f"/verify/status/{TASK}"): [RUNNING, DONE], + ("GET", "/verify/status/t2"): [S503, (200, {"status": "completed", "task_id": "t2", "result": {}})], + }, + ), + "review_and_wait": ( + lambda c, o: c.review_and_wait(DRAFT, idempotency_key=PINNED, **o), + { + ("POST", "/review"): [S503, (202, {"review_id": RID, "status": "queued"})], + ("GET", f"/reviews/{RID}"): [(200, REVIEW_RUNNING), S503, (200, REVIEW_DONE)], + }, + ), + "citecheck_and_wait": ( + lambda c, o: c.citecheck_and_wait(DRAFT, idempotency_key=PINNED, **o), + { + ("POST", "/citecheck"): [S503, (202, {"citecheck_id": CID, "status": "queued"})], + ("GET", f"/citechecks/{CID}"): [S503, (200, CHECK_DONE)], + }, + ), + "verifications.list": ( + lambda c, o: c.verifications.list(page=1, **o), + {("GET", "/verifications"): [S503, PAGE_1]}, + ), + "verifications.iter": ( + lambda c, o: list(c.verifications.iter(**o)), + {("GET", "/verifications"): [S503, PAGE_1, S503, PAGE_2]}, + ), + "verifications.get": (lambda c, o: c.verifications.get("v1", **o), {("GET", "/verifications/v1"): [S503, DETAIL]}), + "verifications.get_certificate": ( + lambda c, o: c.verifications.get_certificate("v1", **o), + {("GET", "/verifications/v1/certificate"): [S503, CERT]}, + ), + "verifications.delete": ( + lambda c, o: c.verifications.delete("v1", **o), + {("DELETE", "/verifications/v1"): [S503, (204, None)]}, + ), + "verifications.related": ( + lambda c, o: c.verifications.related("v1", limit=3, **o), + {("GET", "/verifications/v1/related"): [S503, RELATED]}, + ), + "ask.history": (lambda c, o: c.ask.history("v1", **o), {("GET", "/ask/v1"): [S503, ASK_HISTORY]}), + "ask.send": ( + lambda c, o: c.ask.send("v1", message="Why?", idempotency_key=PINNED, **o), + {("POST", "/ask/v1"): [S503, ASK_REPLY]}, + ), + "ask.reset": (lambda c, o: c.ask.reset("v1", **o), {("DELETE", "/ask/v1"): [S503, (204, None)]}), + "library.list": (lambda c, o: c.library.list(search="x", **o), {("GET", "/library"): [S503, LIB_1]}), + "library.iter": ( + lambda c, o: list(c.library.iter(search="x", **o)), + {("GET", "/library"): [S503, LIB_1, S503, LIB_2]}, + ), +} + + +def _options_for(name: str, *, headers: dict[str, str | None] | None = None) -> dict[str, Any]: + """Every option the method takes, set: a per-attempt timeout of 7 s, two + retries and a marker header.""" + kind = PARITY[name][1] + opts: dict[str, Any] = {"extra_headers": headers if headers is not None else {MARK: "m"}} + if kind in ("call", "submit_wait"): + opts["max_retries"] = 2 + if kind == "call": + opts["timeout"] = 7 + return opts + + +def _run(monkeypatch: pytest.MonkeyPatch, name: str, opts: dict[str, Any], client: Lenz | None = None) -> Any: + call, answers = CALLS[name] + with recorded(answers) as rec: + c = client or Lenz(api_key=API_KEY) + call(c, opts) + return rec + + +def test_every_method_with_options_is_exercised() -> None: + assert sorted(CALLS) == sorted(n for n, (_, kind) in PARITY.items() if kind not in ("with_options", "lifecycle")) + + +@pytest.mark.parametrize("name", sorted(CALLS)) +def test_options_reach_every_request_the_method_makes(monkeypatch: pytest.MonkeyPatch, name: str) -> None: + rec = _run(monkeypatch, name, _options_for(name)) + assert len(rec.requests) >= 2 # a retry, a poll or a second page + for request in rec.requests: + assert _headers(request).get(MARK.lower()) == "m", (name, request["url"]) + + +@pytest.mark.parametrize("name", sorted(CALLS)) +def test_a_copys_options_reach_every_request_too(monkeypatch: pytest.MonkeyPatch, name: str) -> None: + root = Lenz(api_key=API_KEY) + rec = _run(monkeypatch, name, {}, client=root.with_options(extra_headers={MARK: "copy"})) + for request in rec.requests: + assert _headers(request).get(MARK.lower()) == "copy", (name, request["url"]) + + +@pytest.mark.parametrize("name", sorted(CALLS)) +def test_options_change_nothing_but_the_headers_asked_for(monkeypatch: pytest.MonkeyPatch, name: str) -> None: + """Empty options send exactly the request of a call without them; set + options send the same URLs and bodies, with only the marker header + added. The timeouts differ only where a per-attempt timeout was given.""" + plain = _run(monkeypatch, name, {}).requests + empty_opts: dict[str, Any] = {"extra_headers": {}} + if PARITY[name][1] != "wait": + empty_opts["max_retries"] = None + if PARITY[name][1] == "call": + empty_opts["timeout"] = None + empty = _run(monkeypatch, name, empty_opts).requests + assert empty == plain + + full = _run(monkeypatch, name, _options_for(name)).requests + assert [(r["method"], r["url"], r["body"]) for r in full] == [(r["method"], r["url"], r["body"]) for r in plain] + for with_opts, without in zip(full, plain, strict=True): + assert [h for h in with_opts["headers"] if h[0] != MARK] == without["headers"] + assert [h for h in with_opts["headers"] if h[0] == MARK] == [[MARK, "m"]] + if PARITY[name][1] == "call": + assert with_opts["timeout"] == {"connect": 7, "read": 7, "write": 7, "pool": 7} + else: + assert with_opts["timeout"] == without["timeout"] + + +@pytest.mark.parametrize("name", sorted(n for n in CALLS if PARITY[n][1] != "wait")) +def test_max_retries_reaches_the_request_it_retries(name: str) -> None: + """Every scripted call answers 503 first: with ``max_retries=0`` that is + the one request made, and the call fails with it.""" + call, answers = CALLS[name] + with recorded(answers) as rec, pytest.raises(lenz_io.LenzAPIError): + call(Lenz(api_key=API_KEY), {"max_retries": 0}) + assert len(rec.requests) == 1 + + +# ── the budget table: what each option reaches ───────────────────────────── + + +def _count(monkeypatch: pytest.MonkeyPatch, answers: Answers, call: Callable[[Lenz], Any], client: Lenz) -> Any: + with recorded(answers) as rec: + try: + call(client) + except lenz_io.LenzError: + pass + return rec + + +class TestBudgets: + def test_max_retries_replaces_the_clients_count(self, monkeypatch: pytest.MonkeyPatch) -> None: + answers: Answers = {("GET", "/me/usage"): [S503]} + client = Lenz(api_key=API_KEY) + assert len(_count(monkeypatch, answers, lambda c: c.usage(), client).requests) == 4 + assert len(_count(monkeypatch, answers, lambda c: c.usage(max_retries=0), client).requests) == 1 + assert len(_count(monkeypatch, answers, lambda c: c.usage(max_retries=5), client).requests) == 6 + assert len(_count(monkeypatch, answers, lambda c: c.with_options(max_retries=1).usage(), client).requests) == 2 + copy = client.with_options(max_retries=1) + assert len(_count(monkeypatch, answers, lambda c: copy.usage(max_retries=0), client).requests) == 1 + + def test_a_waits_max_retries_is_the_submits_and_never_the_polls(self, monkeypatch: pytest.MonkeyPatch) -> None: + answers: Answers = { + ("POST", "/verify"): [S503, S503, ACCEPTED], + ("GET", f"/verify/status/{TASK}"): [S503, S503, DONE], + } + rec = _count( + monkeypatch, answers, lambda c: c.verify_and_wait("A.", max_retries=5, timeout=60), Lenz(api_key=API_KEY) + ) + submits = [r for r in rec.requests if r["method"] == "POST"] + polls = [r for r in rec.requests if r["method"] == "GET"] + assert len(submits) == 3 + # Each poll is one request: the two 503s are two rounds, with a poll sleep between. + assert len(polls) == 3 and rec.clock.sleeps[-2:] == [2.0, 4.0] + + def test_max_retries_0_on_a_wait_submits_once(self, monkeypatch: pytest.MonkeyPatch) -> None: + answers: Answers = {("POST", "/review"): [S503]} + rec = _count(monkeypatch, answers, lambda c: c.review_and_wait(DRAFT, max_retries=0), Lenz(api_key=API_KEY)) + assert len(rec.requests) == 1 + + def test_a_per_call_timeout_reaches_each_page(self, monkeypatch: pytest.MonkeyPatch) -> None: + rec = _count( + monkeypatch, + {("GET", "/verifications"): [PAGE_1, PAGE_2]}, + lambda c: list(c.verifications.iter(timeout=4)), + Lenz(api_key=API_KEY), + ) + assert [r["timeout"]["read"] for r in rec.requests] == [4, 4] + + def test_a_waits_timeout_stays_the_budget(self, monkeypatch: pytest.MonkeyPatch) -> None: + # ``timeout=12`` is how long to wait: the polls' own timeout stays the + # client's 30 s, capped by what is left of the 12 s. + rec = _count( + monkeypatch, + {("GET", f"/verify/status/{TASK}"): [RUNNING_NO_HINT]}, + lambda c: c.wait(TASK, timeout=12), + Lenz(api_key=API_KEY), + ) + assert [r["timeout"]["read"] for r in rec.requests] == [12, 10, 6] + + def test_the_submit_of_a_wait_takes_the_attempt_timeout_not_the_budget( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + rec = _count( + monkeypatch, + {("POST", "/verify"): [ACCEPTED], ("GET", f"/verify/status/{TASK}"): [DONE]}, + lambda c: c.with_options(timeout=45).verify_and_wait("A.", timeout=5), + Lenz(api_key=API_KEY), + ) + assert rec.requests[0]["timeout"]["read"] == 45 # the submit: the copy's attempt timeout + assert rec.requests[1]["timeout"]["read"] == 5 # the poll: capped by the wait + + +# ── precedence ───────────────────────────────────────────────────────────── + + +def _timeout_of(monkeypatch: pytest.MonkeyPatch, call: Callable[[], Any], answers: Answers) -> dict[str, Any]: + with recorded(answers) as rec: + call() + return rec.requests[0]["timeout"] + + +def _all(value: float | None) -> dict[str, float | None]: + return {"connect": value, "read": value, "write": value, "pool": value} + + +_USAGE: Answers = {("GET", "/me/usage"): [USAGE]} +_EXTRACT: Answers = {("POST", "/extract"): [EXTRACTED]} +_ASSESS: Answers = {("POST", "/assess"): [ASSESSED]} + + +class TestTimeoutPrecedence: + @pytest.mark.parametrize( + ("build", "expected"), + [ + (lambda c: c.usage(), _all(30.0)), + (lambda c: c.usage(timeout=7), _all(7)), + (lambda c: c.usage(timeout=None), _all(30.0)), # None per call = inherit + (lambda c: c.usage(timeout=httpx.Timeout(None)), _all(None)), # unbounded for one call + (lambda c: c.usage(timeout=httpx.Timeout(3, read=9)), {"connect": 3, "read": 9, "write": 3, "pool": 3}), + (lambda c: c.with_options(timeout=12).usage(), _all(12)), + (lambda c: c.with_options(timeout=12).usage(timeout=7), _all(7)), + (lambda c: c.with_options(timeout=12).usage(timeout=None), _all(12)), + (lambda c: c.with_options(timeout=None).usage(), _all(None)), # None on a copy = unbounded + (lambda c: c.with_options(timeout=None).with_options().usage(), _all(None)), # NOT_GIVEN keeps it + (lambda c: c.with_options(timeout=12).with_options(max_retries=0).usage(), _all(12)), + (lambda c: c.with_options(timeout=12).with_options(timeout=20).usage(), _all(20)), + (lambda c: c.with_options(timeout=12).with_options(timeout=NOT_GIVEN).usage(), _all(12)), + ], + ) + def test_a_plain_call(self, monkeypatch: pytest.MonkeyPatch, build: Any, expected: dict[str, Any]) -> None: + client = Lenz(api_key=API_KEY) + assert _timeout_of(monkeypatch, lambda: build(client), _USAGE) == expected + + @pytest.mark.parametrize( + ("constructor", "build", "expected"), + [ + (30.0, lambda c: c.extract(text="Doc."), _all(EXTRACT_TIMEOUT)), + (30.0, lambda c: c.extract(text="Doc.", timeout=5), _all(5)), # explicit: below the floor (D6) + (30.0, lambda c: c.with_options(timeout=200).extract(text="Doc."), _all(200)), + (30.0, lambda c: c.with_options(timeout=20).extract(text="Doc."), _all(EXTRACT_TIMEOUT)), + (30.0, lambda c: c.with_options(timeout=None).extract(text="Doc."), _all(None)), + (30.0, lambda c: c.with_options(timeout=20).extract(text="Doc.", timeout=9), _all(9)), + ( + 30.0, + lambda c: c.with_options(timeout=httpx.Timeout(200, read=5)).extract(text="Doc."), + _all(EXTRACT_TIMEOUT), + ), + ( + 30.0, + lambda c: c.with_options(timeout=httpx.Timeout(5, read=200)).extract(text="Doc."), + {"connect": 5, "read": 200, "write": 5, "pool": 5}, + ), + (300.0, lambda c: c.extract(text="Doc."), _all(300.0)), + (300.0, lambda c: c.with_options(timeout=20).extract(text="Doc."), _all(EXTRACT_TIMEOUT)), + ], + ) + def test_extract_floor_applies_to_an_inherited_timeout_only( + self, monkeypatch: pytest.MonkeyPatch, constructor: float, build: Any, expected: dict[str, Any] + ) -> None: + client = Lenz(api_key=API_KEY, timeout=constructor) + assert _timeout_of(monkeypatch, lambda: build(client), _EXTRACT) == expected + + def test_assess_floor_on_a_copy(self, monkeypatch: pytest.MonkeyPatch) -> None: + client = Lenz(api_key=API_KEY) + assert _timeout_of(monkeypatch, lambda: client.with_options(timeout=50).assess("A."), _ASSESS) == _all(100.0) + assert _timeout_of(monkeypatch, lambda: client.assess("A.", timeout=20), _ASSESS) == _all(20) + + def test_a_borrowed_clients_own_timeout_counts_below_a_copy(self, monkeypatch: pytest.MonkeyPatch) -> None: + client = Lenz(api_key=API_KEY, http_client=httpx.Client(timeout=300.0)) + assert _timeout_of(monkeypatch, lambda: client.extract(text="Doc."), _EXTRACT) == _all(300.0) + assert _timeout_of(monkeypatch, lambda: client.with_options(timeout=40).usage(), _USAGE) == _all(40) + + def test_a_copys_timeout_bounds_a_waits_polls(self, monkeypatch: pytest.MonkeyPatch) -> None: + client = Lenz(api_key=API_KEY).with_options(timeout=httpx.Timeout(4, read=8)) + with recorded({("GET", f"/verify/status/{TASK}"): [RUNNING_NO_HINT, DONE]}) as rec: + client.wait(TASK, timeout=60) + assert [r["timeout"] for r in rec.requests] == [{"connect": 4, "read": 8, "write": 4, "pool": 4}] * 2 + + def test_a_zero_budget_poll_uses_the_copys_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: + client = Lenz(api_key=API_KEY).with_options(timeout=9) + with recorded({("GET", f"/verify/status/{TASK}"): [DONE]}) as rec: + client.wait(TASK, timeout=0) + assert rec.requests[0]["timeout"] == _all(9) + + +# ── headers ──────────────────────────────────────────────────────────────── + + +def _sent_headers(monkeypatch: pytest.MonkeyPatch, call: Callable[[], Any]) -> list[list[str]]: + with recorded(_USAGE) as rec: + call() + return rec.requests[0]["headers"] + + +class TestHeaders: + def test_merged_case_insensitively_the_calls_spelling_and_value_win(self, monkeypatch: pytest.MonkeyPatch) -> None: + copy = Lenz(api_key=API_KEY).with_options(extra_headers={"x-a": "copy", "X-B": "b"}) + sent = _sent_headers(monkeypatch, lambda: copy.usage(extra_headers={"X-A": "call"})) + names = [h[0] for h in sent] + assert ["X-A", "call"] in sent and ["X-B", "b"] in sent + assert [n.lower() for n in names].count("x-a") == 1 + + def test_a_copy_of_a_copy_merges_over_its_parent(self, monkeypatch: pytest.MonkeyPatch) -> None: + parent = Lenz(api_key=API_KEY).with_options(extra_headers={"X-A": "1", "X-B": "2"}) + child = parent.with_options(extra_headers={"x-b": "3", "X-A": None}) + sent = _sent_headers(monkeypatch, child.usage) + assert ["x-b", "3"] in sent + assert "x-a" not in [h[0].lower() for h in sent] + # The parent is unchanged. + assert ["X-A", "1"] in _sent_headers(monkeypatch, parent.usage) + + def test_none_removes_a_copy_header_and_nothing_else(self, monkeypatch: pytest.MonkeyPatch) -> None: + copy = Lenz(api_key=API_KEY).with_options(extra_headers={"X-A": "1"}) + sent = _sent_headers(monkeypatch, lambda: copy.usage(extra_headers={"x-a": None, "User-Agent": None})) + lower = [h[0].lower() for h in sent] + assert "x-a" not in lower + assert "user-agent" in lower # a default is not removed by None + + def test_an_option_header_replaces_a_default_in_any_casing(self, monkeypatch: pytest.MonkeyPatch) -> None: + client = Lenz(api_key=API_KEY) + sent = _sent_headers(monkeypatch, lambda: client.usage(extra_headers={"user-agent": "mine/1", "ACCEPT": "x/y"})) + uas = [h for h in sent if h[0].lower() == "user-agent"] + accepts = [h for h in sent if h[0].lower() == "accept"] + assert uas == [["user-agent", "mine/1"]] and accepts == [["ACCEPT", "x/y"]] + + def test_content_type_is_still_sent_on_a_bodyless_request(self, monkeypatch: pytest.MonkeyPatch) -> None: + sent = _sent_headers(monkeypatch, lambda: Lenz(api_key=API_KEY).usage(extra_headers={MARK: "m"})) + assert ["Content-Type", "application/json"] in sent + + def test_the_shared_client_is_never_changed(self, monkeypatch: pytest.MonkeyPatch) -> None: + root = Lenz(api_key=API_KEY) + before = (list(root._client.headers.raw), root._client.timeout) + copy = root.with_options(timeout=3, max_retries=0, extra_headers={"User-Agent": "x", MARK: "m"}) + _sent_headers(monkeypatch, copy.usage) + assert (list(root._client.headers.raw), root._client.timeout) == before + assert MARK.lower() not in [h[0].lower() for h in _sent_headers(monkeypatch, root.usage)] + + @pytest.mark.parametrize( + "name", + [ + "X-Lenz-API-Version", + "idempotency-key", + "Authorization", + "content-type", + "Content-Length", + "HOST", + "Transfer-Encoding", + ], + ) + def test_reserved_names_are_refused_everywhere(self, name: str) -> None: + client = Lenz(api_key=API_KEY) + for call in ( + lambda: client.usage(extra_headers={name: "x"}), + lambda: client.verify("A.", extra_headers={name: "x"}), + lambda: client.wait(TASK, extra_headers={name: "x"}), + lambda: client.review_and_wait(DRAFT, extra_headers={name: "x"}), + lambda: client.verifications.iter(extra_headers={name: "x"}), + lambda: client.with_options(extra_headers={name: "x"}), + ): + with pytest.raises(ValueError, match="set by the SDK"): + call() + + +# ── ownership of the shared pool ─────────────────────────────────────────── + + +class TestOwnership: + def test_the_root_that_made_the_pool_closes_it(self) -> None: + root = Lenz(api_key=API_KEY) + root.close() + assert root._client.is_closed + + def test_a_borrowed_pool_is_never_closed(self) -> None: + http = httpx.Client() + root = Lenz(api_key=API_KEY, http_client=http) + root.with_options(timeout=5).close() + root.close() + assert not http.is_closed + http.close() + + def test_a_copys_close_and_with_do_nothing(self, monkeypatch: pytest.MonkeyPatch) -> None: + root = Lenz(api_key=API_KEY) + with root.with_options(max_retries=0) as copy: + copy.close() + assert not root._client.is_closed + sibling = root.with_options(timeout=5) + root.with_options().close() + with recorded(_USAGE): + sibling.usage() + root.close() + + def test_a_copy_after_the_root_closed_fails_with_httpxs_error(self) -> None: + root = Lenz(api_key=API_KEY) + copy = root.with_options(timeout=5) + root.close() + with pytest.raises(RuntimeError, match="closed"): + copy.usage() + + def test_parent_and_siblings_are_isolated(self, monkeypatch: pytest.MonkeyPatch) -> None: + root = Lenz(api_key=API_KEY) + a = root.with_options(timeout=3) + b = root.with_options(timeout=4) + assert _timeout_of(monkeypatch, a.usage, _USAGE) == _all(3) + assert _timeout_of(monkeypatch, b.usage, _USAGE) == _all(4) + assert _timeout_of(monkeypatch, root.usage, _USAGE) == _all(30.0) + assert root._client is a._client is b._client + + def test_a_subclass_and_its_namespaces_are_kept(self) -> None: + class MyLenz(Lenz): + def hello(self) -> str: + return "hi" + + root = MyLenz(api_key=API_KEY) + copy = root.with_options(timeout=5) + assert isinstance(copy, MyLenz) and copy.hello() == "hi" + assert copy.verifications._p is copy and copy.ask._p is copy and copy.library._p is copy + assert root.verifications._p is root + + +# ── validation ───────────────────────────────────────────────────────────── + +_BAD_TIMEOUTS = [0, -1, 0.0, math.nan, math.inf, -math.inf, True, "5"] +_BAD_RETRIES = [-1, 1.5, True, "2"] +_BAD_HEADERS: list[Any] = [["X-A"], {"X-A": 5}, {5: "x"}, {"": "x"}] + + +@pytest.fixture() +def no_key_minted(monkeypatch: pytest.MonkeyPatch) -> None: + def refuse() -> Any: + raise AssertionError("a key was minted before validation") + + monkeypatch.setattr("lenz_io.client.uuid.uuid4", refuse) + + +def _per_call_entry_points(client: Lenz) -> list[Callable[..., Any]]: + return [ + lambda **o: client.verify("A.", **o), + lambda **o: client.assess("A.", **o), + lambda **o: client.extract(text="Doc.", **o), + lambda **o: client.review(DRAFT, **o), + lambda **o: client.citecheck(DRAFT, **o), + lambda **o: client.ask.send("v1", message="Why?", **o), + lambda **o: client.usage(**o), + lambda **o: client.verifications.iter(**o), + lambda **o: client.library.iter(**o), + ] + + +class TestValidation: + @pytest.mark.parametrize("bad", _BAD_TIMEOUTS) + def test_a_bad_timeout_is_refused_before_any_request(self, bad: Any, no_key_minted: None) -> None: + client = Lenz(api_key=API_KEY) + with httpx_refused(): + for call in _per_call_entry_points(client): + with pytest.raises(ValueError, match="timeout"): + call(timeout=bad) + with pytest.raises(ValueError, match="timeout"): + client.with_options(timeout=bad) + with pytest.raises(ValueError, match="timeout"): + Lenz(api_key=API_KEY, timeout=bad) + + @pytest.mark.parametrize("bad", _BAD_RETRIES) + def test_a_bad_retry_count_is_refused_before_any_request(self, bad: Any, no_key_minted: None) -> None: + client = Lenz(api_key=API_KEY) + with httpx_refused(): + for call in _per_call_entry_points(client): + with pytest.raises(ValueError, match="max_retries"): + call(max_retries=bad) + for call in ( + lambda: client.verify_and_wait("A.", max_retries=bad), + lambda: client.verify_batch_and_wait(claims=[{"claim": "A."}], max_retries=bad), + lambda: client.review_and_wait(DRAFT, max_retries=bad), + lambda: client.citecheck_and_wait(DRAFT, max_retries=bad), + lambda: client.with_options(max_retries=bad), + lambda: Lenz(api_key=API_KEY, max_retries=bad), + ): + with pytest.raises(ValueError, match="max_retries"): + call() + + def test_with_options_refuses_none_retries(self) -> None: + with pytest.raises(ValueError, match="max_retries"): + Lenz(api_key=API_KEY).with_options(max_retries=None) # type: ignore[arg-type] + + @pytest.mark.parametrize("bad", _BAD_HEADERS) + def test_bad_headers_are_refused_before_any_request(self, bad: Any, no_key_minted: None) -> None: + client = Lenz(api_key=API_KEY) + with httpx_refused(): + for call in _per_call_entry_points(client): + with pytest.raises(ValueError): + call(extra_headers=bad) + for call in ( + lambda: client.wait(TASK, extra_headers=bad), + lambda: client.verify_and_wait("A.", extra_headers=bad), + lambda: client.with_options(extra_headers=bad), + ): + with pytest.raises(ValueError): + call() + + @pytest.mark.parametrize("good", [1, 0.5, 30, httpx.Timeout(None), httpx.Timeout(5, read=None), None]) + def test_good_timeouts_are_accepted(self, good: Any) -> None: + Lenz(api_key=API_KEY, timeout=good).with_options(timeout=good).close() + + def test_wait_budgets_are_not_newly_validated(self, monkeypatch: pytest.MonkeyPatch) -> None: + # ``<= 0`` keeps meaning "poll once". + with recorded({("GET", f"/verify/status/{TASK}"): [DONE]}) as rec: + Lenz(api_key=API_KEY).wait(TASK, timeout=-5) + assert len(rec.requests) == 1 + + def test_not_given_is_one_falsy_object(self) -> None: + assert NotGiven() is NOT_GIVEN and not NOT_GIVEN and repr(NOT_GIVEN) == "NOT_GIVEN" + assert lenz_io.NOT_GIVEN is NOT_GIVEN + + +@contextmanager +def httpx_refused() -> Iterator[None]: + """No request may be sent inside the block.""" + import respx + + with respx.mock(base_url=BASE, assert_all_called=False) as router: + router.route().mock(side_effect=AssertionError("a request was sent")) + yield diff --git a/tests/test_smoke_staging.py b/tests/test_smoke_staging.py index b866ce1..a32e47c 100644 --- a/tests/test_smoke_staging.py +++ b/tests/test_smoke_staging.py @@ -11,6 +11,7 @@ cheap run (~60s); a cache hit is a bonus, never assumed 4. ``ask.history`` — read-only follow-up surface (no exchange burned) 5. ``cancel`` — a ``depth="low"`` run stopped right after it starts + 6. request options — ``with_options`` and a per-call ``timeout`` on ``assess`` Plus webhook signature roundtrip + ``/me/usage`` shape. @@ -91,6 +92,15 @@ def test_assess_returns_typed_claims(smoke_client): assert first.confidence in ("high", "medium", "low") +def test_request_options_reach_the_live_api(smoke_client): + """The request options on a real call: a copy with its own retries and an + extra header, and a per-call timeout (the same claim as above, so the + API's verdict memo usually answers it).""" + copy = smoke_client.with_options(max_retries=1, extra_headers={"X-Smoke-Check": "request-options"}) + out = copy.assess(claim="Sharks don't get cancer", timeout=120) + assert out.claims and out.claims[0].verdict + + def test_webhook_signature_roundtrip(): """The signing path matches the server. Cross-check against a known payload.""" secret = "whsec_smoke_fixed" From 597f32d773e7f3207e11fd56feba49c7e6d43bc4 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 23:51:43 +0300 Subject: [PATCH 57/67] docs: per-call options, with_options, and the validation rule README: Per-call options and with_options under Configuration (what each option reaches per method, precedence, None vs not passed, the floors, reserved headers, who closes the shared pool); the async section says cancelling a to_thread task does not stop the call. CHANGELOG: Added and Changed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 26 ++++++++++++++++ README.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57f4311..0d0c39b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,17 @@ Deprecated). ### Changed +- **One rule for every timeout and retry count of a request, checked before + anything is sent.** A timeout must be a finite number of seconds greater + than 0, `None` (no timeout) or an `httpx.Timeout`; a retry count a whole + number, 0 or more (`True` / `False` are refused). Anything else raises + `ValueError`: in `Lenz(timeout=..., max_retries=...)` when the client is + built, and in the `timeout=` of `extract` / `assess` before the call mints a + key or sends a request. These values did not work before (a zero or + negative timeout failed every request, a negative retry count sent none), + except `True` / `False`, which were read as 1 / 0: pass the number. + The wait helpers' `timeout` (how long to wait) is not affected: `0` or less + still reads once. - **An id goes into the URL path as one segment.** Every method that puts an id in a path (`get_status`, `select`, `get_review`, `get_citecheck`, the cancels, `verifications.*`, `ask.*`, the waits) percent-encodes it whole, so @@ -187,6 +198,21 @@ Deprecated). ### Added +- **Per-call request options and `with_options`.** Every method takes three + keyword-only options for that call: `timeout` (one HTTP attempt, seconds or + an `httpx.Timeout`; `None` keeps the client's), `max_retries` and + `extra_headers` (added to every request the call makes; the SDK's own + headers are refused). On the wait helpers `timeout` stays how long to wait, + `max_retries` is the submit's, and `wait` takes `extra_headers` only. + `client.with_options(timeout=..., max_retries=..., extra_headers=...)` + returns a copy with other defaults that shares the connection pool; closing + a copy does nothing. Per option the call wins over the copy and the copy + over the client; headers merge, and `None` removes one a copy added. The + `extract` / `assess` floors (150 s / 100 s) apply to an inherited timeout + only. A call that passes no option sends exactly the request it sent before. + `NOT_GIVEN` / `NotGiven` (the default of `with_options`) are exported for + type annotations. The `timeout=` of `extract` / `assess` now also takes an + `httpx.Timeout`. See "Per-call options" in the README. - **Stopping a run: `cancel`, `cancel_review` and `cancel_citecheck`.** `client.cancel(task_id)` stops a verification and returns a `CancelResult` (`task_id`, `cancelled`, `status`); `client.cancel_review(review_id)` stops a diff --git a/README.md b/README.md index 68775dc..f5e517d 100644 --- a/README.md +++ b/README.md @@ -1202,6 +1202,12 @@ def check(claim: str) -> dict[str, object]: # `def`, not `async def`: FastAPI r return {"verdict": v.verdict, "score": v.lenz_score} ``` +Cancelling the asyncio task that awaits `asyncio.to_thread(...)` does not stop the call: the +thread runs on until the call returns. Bound each request with `timeout=` (see +[Per-call options](#per-call-options)), and stop paid work on the server with `cancel()`, +`cancel_review()` or `cancel_citecheck()` (see [Stopping a run](#stopping-a-run)). A native +async client is planned. + For a long check behind a web request, `verify(..., webhook_url=...)` returns at once and Lenz posts the result to you when it is done (see [Webhooks](#webhooks)). That ties up no thread for the wait. To check many claims at once, send them in one call @@ -1225,6 +1231,83 @@ Environment variables: An OAuth access token for the Lenz API works wherever the API key goes: pass it as `api_key` or in `LENZ_API_KEY`. +`timeout` must be a number of seconds greater than 0, `None` (no timeout) or an +`httpx.Timeout`; `max_retries` a whole number, 0 or more. Anything else raises +`ValueError` when the client is built. + +### Per-call options + +Every method takes three keyword-only request options, for that call only: + +```python +client.assess(claim="...", timeout=20) # one HTTP attempt, in seconds +client.usage(max_retries=0) # no retries for this call +client.verify("...", extra_headers={"X-Trace-Id": trace}) # added to the request +``` + +- `timeout`: one HTTP attempt, a number of seconds greater than 0 or an + `httpx.Timeout`. httpx applies it per phase (connect, read, write, pool), and + the read limit to each chunk of the answer: it limits inactivity, not the + whole call, and each retry gets its own. +- `max_retries`: how often a request that failed in a way worth retrying (a + 5xx, a 429, a dropped connection) is sent again: a whole number, 0 or more. +- `extra_headers`: headers added to the request. The SDK's own headers are + refused (`X-Lenz-API-Version`, `Idempotency-Key`, `Authorization`, + `Content-Type`, `Content-Length`, `Host`, `Transfer-Encoding`): use + `idempotency_key=` and `api_key=` for the first two. A header with the name + of a default one (`User-Agent`, `Accept`) replaces it. + +What each option reaches: + +| Methods | `timeout` | `max_retries` | `extra_headers` | +|---|---|---|---| +| Plain calls (`verify`, `review`, `get_status`, `cancel`, `usage`, `verifications.*`, `ask.*`, `library.list`, ...) | the attempt | the call's retries | every request | +| `extract`, `assess` | the attempt, used as given | the call's retries | every request | +| Waits (`wait`, `verify_and_wait`, `verify_batch_and_wait`, `review_and_wait`, `citecheck_and_wait`) | **how long to wait** (unchanged) | the submit's retries (`wait` has none: each poll is one request) | the submit and every poll | +| `verifications.iter`, `library.iter` | each page's attempt | each page's retries | every page | +| `with_options` | the default attempt timeout of the copy (also what each poll of a wait uses, capped by what is left of the wait) | the copy's default | added to every request of the copy | + +A bad value raises `ValueError` before anything is sent (for the iterators, +when the iterator is created). Precedence, per option: the call's keyword, then +the copy's (`with_options`), then the client's; headers merge, the call's over +the copy's. + +`extract` and `assess` wait at least 150 s and 100 s when the timeout comes +from a copy or the client, as they always did. A timeout passed to the call is +used as given, even below that: it can time out a call the server is still +running, so retry it with the same `idempotency_key` to get its answer. + +`None` means different things in two places: + +| | `None` | not passed | +|---|---|---| +| `timeout=` on a call | the copy's or the client's timeout | the same | +| `with_options(timeout=...)`, `Lenz(timeout=...)` | no timeout | keep the current one (the client default is 30 s) | +| `extra_headers={"X-A": None}` | removes `X-A` added by a copy | — | + +For no timeout on one call, pass `timeout=httpx.Timeout(None)`. + +### A client with other defaults: `with_options` + +`client.with_options(...)` returns a copy with its own defaults, sharing the +connection pool, key and base URL. It is cheap, so you can make one per request +or per job, and the client it was made from does not change: + +```python +fast = client.with_options(timeout=10, max_retries=0) +fast.assess(claim="...") + +for job in jobs: + scoped = client.with_options(extra_headers={"X-Job-Id": job.id}) + scoped.review_and_wait(job.draft) +``` + +A copy of a copy starts from the copy's options. The pool belongs to the +client that created it: `close()` and `with` on a copy do nothing, closing the +original closes the pool for every copy (a copy then raises httpx's +closed-client error), and a client given `http_client=` never closes it. A +copy is as safe to share across threads as the client. + ## Compatibility - Python 3.10, 3.11, 3.12 From e30d1a8c4a4b81ab3bc3e352604b85f432382fc5 Mon Sep 17 00:00:00 2001 From: Kosta Date: Fri, 9 Oct 2026 23:52:22 +0300 Subject: [PATCH 58/67] test: say what the below-the-floor case pins Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- tests/test_request_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_request_options.py b/tests/test_request_options.py index 71ca720..450f010 100644 --- a/tests/test_request_options.py +++ b/tests/test_request_options.py @@ -492,7 +492,7 @@ def test_a_plain_call(self, monkeypatch: pytest.MonkeyPatch, build: Any, expecte ("constructor", "build", "expected"), [ (30.0, lambda c: c.extract(text="Doc."), _all(EXTRACT_TIMEOUT)), - (30.0, lambda c: c.extract(text="Doc.", timeout=5), _all(5)), # explicit: below the floor (D6) + (30.0, lambda c: c.extract(text="Doc.", timeout=5), _all(5)), # explicit: used as given, below the floor (30.0, lambda c: c.with_options(timeout=200).extract(text="Doc."), _all(200)), (30.0, lambda c: c.with_options(timeout=20).extract(text="Doc."), _all(EXTRACT_TIMEOUT)), (30.0, lambda c: c.with_options(timeout=None).extract(text="Doc."), _all(None)), From eca24adad2e17492ef0f841bd8c40998d771d179 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sat, 10 Oct 2026 00:05:04 +0300 Subject: [PATCH 59/67] fix: keep 2.21 overrides working, snapshot options, accept every timeout 2.21 took - review_and_wait / citecheck_and_wait / verify_and_wait and the iterators call the overridable public method (review, citecheck, wait, list) with only the options the caller gave; an empty extra_headers counts as none. A subclass written with the 2.21 signatures keeps working. - Options are checked and snapshotted once: a copy keeps its own httpx.Timeout (given or inherited), an iterator reads every page with the options it was created with, a wait uses one snapshot for the submit and every poll. - Timeouts: httpx's (connect, read, write, pool) tuple and any real number (not bool) are accepted again; retry counts take anything operator.index takes. Header names must be RFC 7230 tokens and values visible ASCII, refused before a key is minted. - A poll request that cannot be built (UnicodeEncodeError) ends a review or citation-check wait instead of being read as an unreadable body. - Lenz.__enter__ returns Self, so a subclass copy keeps its type in a with. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- src/lenz_io/client.py | 181 +++++++++++++++------- tests/test_request_options.py | 282 +++++++++++++++++++++++++++++++++- 2 files changed, 404 insertions(+), 59 deletions(-) diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 1a3f3f1..c0f5c0f 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -82,7 +82,10 @@ import copy import logging import math +import numbers +import operator import os +import re import time import uuid from collections.abc import Callable, Iterator, Mapping, Sequence @@ -92,6 +95,7 @@ from urllib.parse import quote import httpx +from typing_extensions import Self from . import __version__ from .errors import ( @@ -249,41 +253,79 @@ def __deepcopy__(self, memo: Any) -> NotGiven: ) -def _check_timeout(value: Any, where: str) -> None: - """A per-request timeout: a finite number of seconds greater than 0, an - ``httpx.Timeout`` (taken as given) or ``None``. ``ValueError`` otherwise, - before any request is made.""" - if value is None or isinstance(value, httpx.Timeout): - return - if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value <= 0: - raise ValueError( - f"{where}: timeout must be a number of seconds greater than 0, None or an httpx.Timeout (got {value!r})." - ) +def _seconds(value: Any) -> bool: + """A finite real number of seconds greater than 0 (``bool`` is not one).""" + return not isinstance(value, bool) and isinstance(value, numbers.Real) and math.isfinite(value) and float(value) > 0 + + +def _snapshot(value: Any) -> Any: + """A copy of an ``httpx.Timeout`` (they are mutable); anything else as is.""" + return httpx.Timeout(value) if isinstance(value, httpx.Timeout) else value + + +def _check_timeout(value: Any, where: str) -> float | httpx.Timeout | None: + """A per-request timeout, checked and snapshotted: ``None``, a finite real + number of seconds greater than 0, httpx's 4-tuple ``(connect, read, write, + pool)`` whose parts are each ``None`` or such a number, or an + ``httpx.Timeout`` (taken as given). An ``httpx.Timeout`` or a tuple comes + back as a new ``httpx.Timeout``, so changing the caller's object later + changes nothing here. ``ValueError`` otherwise, before any request.""" + if value is None or _seconds(value): + return value # type: ignore[no-any-return] + if isinstance(value, httpx.Timeout): + return httpx.Timeout(value) + if isinstance(value, tuple) and len(value) == 4 and all(part is None or _seconds(part) for part in value): + return httpx.Timeout(value) + raise ValueError( + f"{where}: timeout must be a number of seconds greater than 0, None, an httpx.Timeout or a " + f"(connect, read, write, pool) tuple of such numbers or None (got {value!r})." + ) -def _check_retries(value: Any, where: str) -> None: - """A retry count: a whole number, 0 or more. ``ValueError`` otherwise.""" - if isinstance(value, bool) or not isinstance(value, int) or value < 0: +def _check_retries(value: Any, where: str) -> int: + """A retry count: a whole number (anything ``operator.index`` takes, but + not ``bool``), 0 or more, returned as an ``int``. ``ValueError`` otherwise.""" + count: int | None = None + if not isinstance(value, bool): + try: + count = operator.index(value) + except TypeError: + count = None + if count is None or count < 0: raise ValueError(f"{where}: max_retries must be a whole number, 0 or more (got {value!r}).") + return count + + +#: A header name is an RFC 7230 token; a value is visible ASCII, spaces and +#: tabs (no CR, LF or NUL: those would end the header or the request). +_HEADER_NAME = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+") +_HEADER_VALUE = re.compile(r"[\t\x20-\x7e]*") def _check_headers(value: Any, where: str) -> tuple[tuple[str, str | None], ...]: """``extra_headers`` as (name, value) pairs in the order given: names - strings outside ``_RESERVED_HEADERS``, values strings or ``None`` - (removes the header a ``with_options`` copy added). ``ValueError`` - otherwise.""" + RFC 7230 tokens outside ``_RESERVED_HEADERS``, values visible-ASCII + strings or ``None`` (removes the header a ``with_options`` copy added). + ``ValueError`` otherwise. The pairs are a snapshot: changing the caller's + mapping later changes nothing here.""" if value is None: return () if not isinstance(value, Mapping): raise ValueError(f"{where}: extra_headers must be a mapping of header names to strings (got {value!r}).") pairs: list[tuple[str, str | None]] = [] for name, header in value.items(): - if not isinstance(name, str) or not name.strip(): - raise ValueError(f"{where}: a header name must be a non-empty string (got {name!r}).") + if not isinstance(name, str) or not _HEADER_NAME.fullmatch(name): + raise ValueError( + f"{where}: a header name must be a non-empty token of ASCII letters, digits and " + f"!#$%&'*+-.^_`|~ (got {name!r})." + ) if name.lower() in _RESERVED_HEADERS: raise ValueError(f"{where}: the {name} header is set by the SDK and cannot be passed in extra_headers.") - if header is not None and not isinstance(header, str): - raise ValueError(f"{where}: the value of header {name} must be a string or None (got {header!r}).") + if header is not None and (not isinstance(header, str) or not _HEADER_VALUE.fullmatch(header)): + raise ValueError( + f"{where}: the value of header {name} must be a string of visible ASCII characters, spaces " + f"and tabs, or None." + ) pairs.append((name, header)) return tuple(pairs) @@ -322,12 +364,27 @@ def _call_options( extra_headers: Mapping[str, str | None] | None, where: str, ) -> _CallOptions: - """One call's request options, checked: raises ``ValueError`` before the - call mints a key or makes a request.""" - _check_timeout(timeout, where) - if max_retries is not None: - _check_retries(max_retries, where) - return _CallOptions(timeout=timeout, max_retries=max_retries, headers=_check_headers(extra_headers, where)) + """One call's request options, checked and snapshotted: raises + ``ValueError`` before the call mints a key or makes a request.""" + return _CallOptions( + timeout=_check_timeout(timeout, where), + max_retries=None if max_retries is None else _check_retries(max_retries, where), + headers=_check_headers(extra_headers, where), + ) + + +def _given(options: _CallOptions) -> dict[str, Any]: + """``options`` as keywords for another public method, only those that were + given: a method overridden with a 2.21 signature (which knows none of + them) is still called the way 2.21 called it when there are none.""" + out: dict[str, Any] = {} + if options.timeout is not None: + out["timeout"] = options.timeout + if options.max_retries is not None: + out["max_retries"] = options.max_retries + if options.headers: + out["extra_headers"] = dict(options.headers) + return out @dataclass(frozen=True) @@ -549,11 +606,9 @@ def iter( apply to every page request, and a bad one raises here, before the first page: see :meth:`Lenz.with_options`. """ - _call_options(timeout, max_retries, extra_headers, "verifications.iter()") - return _walk( - lambda n: self.list(page=n, timeout=timeout, max_retries=max_retries, extra_headers=extra_headers), - _first_page(page), - ) + # Checked and snapshotted now: every page is read with these. + options = _call_options(timeout, max_retries, extra_headers, "verifications.iter()") + return _walk(lambda n: self.list(page=n, **_given(options)), _first_page(page)) def get( self, @@ -893,7 +948,8 @@ def iter( apply to every page request, and a bad one raises here, before the first page: see :meth:`Lenz.with_options`. """ - _call_options(timeout, max_retries, extra_headers, "library.iter()") + # Checked and snapshotted now: every page is read with these. + options = _call_options(timeout, max_retries, extra_headers, "library.iter()") if sort == "random": raise ValueError('iter cannot walk sort="random" (each page is a fresh sample); call library.list instead.') page = _first_page(page) @@ -906,9 +962,7 @@ def iter( entity=entity, curated=curated, verdict=verdict, - timeout=timeout, - max_retries=max_retries, - extra_headers=extra_headers, + **_given(options), ), page, ) @@ -1020,8 +1074,8 @@ def __init__( ) -> None: # The same rule as every request option: refused here, before the # client exists, rather than failing (or retrying forever) later. - _check_timeout(timeout, "Lenz()") - _check_retries(max_retries, "Lenz()") + timeout = _check_timeout(timeout, "Lenz()") + max_retries = _check_retries(max_retries, "Lenz()") self._api_key = api_key or os.environ.get("LENZ_API_KEY") or "" self._base_url = (base_url or os.environ.get("LENZ_BASE_URL") or DEFAULT_BASE_URL).rstrip("/") self._timeout = timeout @@ -1107,15 +1161,16 @@ def with_options( copy is as safe to share across threads as the client. """ if not isinstance(timeout, NotGiven): - _check_timeout(timeout, "with_options()") + timeout = _check_timeout(timeout, "with_options()") if not isinstance(max_retries, NotGiven): - _check_retries(max_retries, "with_options()") + max_retries = _check_retries(max_retries, "with_options()") headers = _check_headers(extra_headers, "with_options()") layer = self._options clone = copy.copy(self) clone._owns_client = False clone._options = _ClientOptions( - timeout=layer.timeout if isinstance(timeout, NotGiven) else timeout, + # A snapshot: each copy holds its own ``httpx.Timeout``. + timeout=_snapshot(layer.timeout) if isinstance(timeout, NotGiven) else timeout, max_retries=layer.max_retries if isinstance(max_retries, NotGiven) else max_retries, headers=_merge_headers(layer.headers, headers), ) @@ -1124,7 +1179,7 @@ def with_options( clone.library = _LibraryNamespace(clone) return clone - def __enter__(self) -> Lenz: + def __enter__(self) -> Self: return self def __exit__(self, *exc_info: Any) -> None: @@ -1618,7 +1673,8 @@ def verify_and_wait( timeout of each request comes from the copy or the client, since ``timeout`` here is how long to wait. """ - _call_options(None, max_retries, extra_headers, "verify_and_wait()") + # Checked and snapshotted once: the submit and every poll use these. + options = _call_options(None, max_retries, extra_headers, "verify_and_wait()") key = _call_key(idempotency_key, idempotency) with _carrying_key(key): accepted = self._verify_submit( @@ -1630,14 +1686,16 @@ def verify_and_wait( visibility=visibility, depth=depth, idempotency_key=key, - max_retries=max_retries, - extra_headers=extra_headers, + max_retries=options.max_retries, + extra_headers=dict(options.headers), ) logger.info("Submitted task: %s", accepted.task_id) - if extra_headers is None: - # Called as in 2.x, so a subclass overriding ``wait`` is still called. - return self.wait(accepted, timeout=timeout, on_progress=on_progress) - return self.wait(accepted, timeout=timeout, on_progress=on_progress, extra_headers=extra_headers) + # Only the headers reach the polls, and only when there are some, so + # a subclass overriding ``wait`` with the 2.21 signature is still + # called the way 2.21 called it. + return self.wait( + accepted, timeout=timeout, on_progress=on_progress, **_given(_CallOptions(headers=options.headers)) + ) def wait( self, @@ -1731,6 +1789,7 @@ def verify_batch_and_wait( timeout of each request comes from the copy or the client, since ``timeout`` here is how long to wait. """ + # Checked and snapshotted once: the submit and every poll use these. options = _call_options(None, max_retries, extra_headers, "verify_batch_and_wait()") key = _call_key(idempotency_key, idempotency) with _carrying_key(key): @@ -1741,8 +1800,8 @@ def verify_batch_and_wait( visibility=visibility, depth=depth, idempotency_key=key, - max_retries=max_retries, - extra_headers=extra_headers, + max_retries=options.max_retries, + extra_headers=dict(options.headers), ) ids = [it.task_id for it in accepted.items if it.task_id] terminal, timed_out, stopped = self._poll_to_terminal( @@ -2054,7 +2113,8 @@ def review_and_wait( timeout of each request comes from the copy or the client, since ``timeout`` here is how long to wait. """ - _call_options(None, max_retries, extra_headers, "review_and_wait()") + # Checked and snapshotted once: the submit and every poll use these. + options = _call_options(None, max_retries, extra_headers, "review_and_wait()") # The key is minted here (as ``review`` would) so the wait's errors carry it too. key = idempotency_key or uuid.uuid4().hex with _carrying_key(key): @@ -2071,12 +2131,12 @@ def review_and_wait( webhook_url=webhook_url, visibility=visibility, idempotency_key=key, - max_retries=max_retries, - extra_headers=extra_headers, + # Only the options given, so a 2.21 ``review`` override is still called. + **_given(options), ) logger.info("Submitted review: %s", started.review_id) return self._wait_review( - started.review_id, timeout=timeout, on_update=on_update, extra_headers=extra_headers + started.review_id, timeout=timeout, on_update=on_update, extra_headers=dict(options.headers) ) # ── /citecheck: the citation check on its own ── @@ -2241,7 +2301,8 @@ def citecheck_and_wait( timeout of each request comes from the copy or the client, since ``timeout`` here is how long to wait. """ - _call_options(None, max_retries, extra_headers, "citecheck_and_wait()") + # Checked and snapshotted once: the submit and every poll use these. + options = _call_options(None, max_retries, extra_headers, "citecheck_and_wait()") # The key is minted here (as ``citecheck`` would) so the wait's errors carry it too. key = idempotency_key or uuid.uuid4().hex with _carrying_key(key): @@ -2252,12 +2313,12 @@ def citecheck_and_wait( language=language, webhook_url=webhook_url, idempotency_key=key, - max_retries=max_retries, - extra_headers=extra_headers, + # Only the options given, so a 2.21 ``citecheck`` override is still called. + **_given(options), ) logger.info("Submitted citation check: %s", started.citecheck_id) return self._wait_citecheck( - started.citecheck_id, timeout=timeout, on_update=on_update, extra_headers=extra_headers + started.citecheck_id, timeout=timeout, on_update=on_update, extra_headers=dict(options.headers) ) def _wait_citecheck( @@ -2362,6 +2423,10 @@ def _wait_job( job = parse(body) if job is None: raise ValueError("not this job's body") + except UnicodeEncodeError: + # The request could not be built (nothing was sent): no later + # poll would do better, so it is not read as an unreadable body. + raise except ValueError: # A body this release cannot read (pydantic's ValidationError # is a ValueError): read again next round, as for a 5xx. diff --git a/tests/test_request_options.py b/tests/test_request_options.py index 450f010..05673b4 100644 --- a/tests/test_request_options.py +++ b/tests/test_request_options.py @@ -18,7 +18,7 @@ import sys from collections.abc import Callable, Iterator from contextlib import contextmanager -from typing import Any +from typing import Any, ClassVar import httpx import pytest @@ -776,3 +776,283 @@ def httpx_refused() -> Iterator[None]: with respx.mock(base_url=BASE, assert_all_called=False) as router: router.route().mock(side_effect=AssertionError("a request was sent")) yield + + +# ── review follow-ups: 2.21 overrides, snapshots, the httpx tuple form ───── + + +class Old221(Lenz): + """Overrides with the 2.21 signatures, which know no request option.""" + + def __init__(self, **kw: Any) -> None: + super().__init__(**kw) + self.called: list[str] = [] + + def review( # type: ignore[override] + self, + text: str, + *, + verdicts: list[str] | None = None, + confidence: list[str] | None = None, + max_assessments: int | None = None, + max_verifications: int | None = None, + depth: str | None = None, + max_citations: int | None = None, + suggest_edits: bool = False, + language: str = "", + webhook_url: str | None = None, + visibility: str = "private", + idempotency_key: str | None = None, + ) -> Any: + self.called.append("review") + return super().review( + text, + verdicts=verdicts, + confidence=confidence, + max_assessments=max_assessments, + max_verifications=max_verifications, + depth=depth, + max_citations=max_citations, + suggest_edits=suggest_edits, + language=language, + webhook_url=webhook_url, + visibility=visibility, + idempotency_key=idempotency_key, + ) + + def citecheck( # type: ignore[override] + self, + text: str | None = None, + *, + pairs: Any = None, + max_citations: int | None = None, + language: str = "", + webhook_url: str | None = None, + idempotency_key: str | None = None, + ) -> Any: + self.called.append("citecheck") + return super().citecheck( + text, + pairs=pairs, + max_citations=max_citations, + language=language, + webhook_url=webhook_url, + idempotency_key=idempotency_key, + ) + + def wait(self, task: Any, *, timeout: float = WAIT_TIMEOUT, on_progress: Any = None) -> Any: # type: ignore[override] + self.called.append("wait") + return super().wait(task, timeout=timeout, on_progress=on_progress) + + +class _OldVerifications(lenz_io.client._VerificationsNamespace): + def list(self, *, page: int = 1) -> Any: # type: ignore[override] + return super().list(page=page) + + +class _OldLibrary(lenz_io.client._LibraryNamespace): + def list( # type: ignore[override] + self, + *, + page: int = 1, + sort: str = "recent", + search: str = "", + domain: str = "", + entity: str = "", + curated: Any = None, + verdict: str = "", + ) -> Any: + return super().list( + page=page, sort=sort, search=search, domain=domain, entity=entity, curated=curated, verdict=verdict + ) + + +_REVIEW_WAIT: Answers = { + ("POST", "/review"): [(202, {"review_id": RID, "status": "queued"})], + ("GET", f"/reviews/{RID}"): [(200, REVIEW_DONE)], +} +_CHECK_WAIT: Answers = { + ("POST", "/citecheck"): [(202, {"citecheck_id": CID, "status": "queued"})], + ("GET", f"/citechecks/{CID}"): [(200, CHECK_DONE)], +} +_VERIFY_WAIT: Answers = {("POST", "/verify"): [ACCEPTED], ("GET", f"/verify/status/{TASK}"): [DONE]} + + +class TestOverridesWritten221: + """A subclass written for 2.21 keeps working through the helpers that call + its overridden methods, as long as the caller passes no option (an empty + ``extra_headers`` counts as none).""" + + @pytest.mark.parametrize("opts", [{}, {"extra_headers": {}}, {"max_retries": None, "extra_headers": None}]) + def test_review_and_wait(self, opts: dict[str, Any]) -> None: + client = Old221(api_key=API_KEY) + with recorded(_REVIEW_WAIT): + client.review_and_wait(DRAFT, **opts) + assert client.called == ["review"] + + @pytest.mark.parametrize("opts", [{}, {"extra_headers": {}}]) + def test_citecheck_and_wait(self, opts: dict[str, Any]) -> None: + client = Old221(api_key=API_KEY) + with recorded(_CHECK_WAIT): + client.citecheck_and_wait(DRAFT, **opts) + assert client.called == ["citecheck"] + + @pytest.mark.parametrize("opts", [{}, {"extra_headers": {}}, {"max_retries": 1}]) + def test_verify_and_wait(self, opts: dict[str, Any]) -> None: + client = Old221(api_key=API_KEY) + with recorded(_VERIFY_WAIT): + client.verify_and_wait("A.", **opts) + assert client.called == ["wait"] + + def test_the_iterators(self) -> None: + client = Lenz(api_key=API_KEY) + client.verifications = _OldVerifications(client) + client.library = _OldLibrary(client) + with recorded({("GET", "/verifications"): [PAGE_1, PAGE_2], ("GET", "/library"): [LIB_1, LIB_2]}): + assert len(list(client.verifications.iter())) == 2 + assert len(list(client.library.iter(extra_headers={}))) == 2 + + def test_an_option_still_reaches_an_override_that_takes_it(self) -> None: + client = Lenz(api_key=API_KEY) + with recorded(_REVIEW_WAIT) as rec: + client.review_and_wait(DRAFT, extra_headers={MARK: "m"}) + assert all(_headers(r).get(MARK.lower()) == "m" for r in rec.requests) + + +class TestSnapshots: + def test_a_copy_keeps_the_timeout_it_was_given(self) -> None: + t = httpx.Timeout(8) + a = Lenz(api_key=API_KEY).with_options(timeout=t) + b = a.with_options() + t.read = None + for client in (a, b): + with recorded(_USAGE) as rec: + client.usage() + assert rec.requests[0]["timeout"] == _all(8) + + def test_an_iterator_keeps_the_options_it_was_created_with(self) -> None: + headers: dict[str, str | None] = {MARK: "first"} + t = httpx.Timeout(4) + with recorded({("GET", "/verifications"): [PAGE_1, PAGE_2]}) as rec: + it = Lenz(api_key=API_KEY).verifications.iter(extra_headers=headers, timeout=t) + headers[MARK] = "changed" + next(it) + headers["X-Late"] = "late" + t.read = 99 + list(it) + assert [_headers(r).get(MARK.lower()) for r in rec.requests] == ["first", "first"] + assert all("x-late" not in _headers(r) for r in rec.requests) + assert [r["timeout"]["read"] for r in rec.requests] == [4, 4] + + def test_the_library_iterator_too(self) -> None: + headers: dict[str, str | None] = {MARK: "first"} + with recorded({("GET", "/library"): [LIB_1, LIB_2]}) as rec: + it = Lenz(api_key=API_KEY).library.iter(extra_headers=headers) + next(it) + headers[MARK] = "changed" + list(it) + assert [_headers(r).get(MARK.lower()) for r in rec.requests] == ["first", "first"] + + +class TestTimeoutForms: + """What 2.21 accepted keeps working: the httpx tuple form and any real + number (not ``bool``).""" + + _TUPLE = (5, 30, 6, 7) + _AS_DICT: ClassVar[dict[str, int]] = {"connect": 5, "read": 30, "write": 6, "pool": 7} + + def test_the_tuple_form_on_the_constructor(self) -> None: + with recorded(_USAGE) as rec: + Lenz(api_key=API_KEY, timeout=self._TUPLE).usage() # type: ignore[arg-type] + assert rec.requests[0]["timeout"] == self._AS_DICT + + def test_the_tuple_form_on_a_call_and_a_copy(self) -> None: + client = Lenz(api_key=API_KEY) + with recorded(_EXTRACT) as rec: + client.extract(text="Doc.", timeout=self._TUPLE) # type: ignore[arg-type] + assert rec.requests[0]["timeout"] == self._AS_DICT + with recorded(_USAGE) as rec: + client.with_options(timeout=self._TUPLE).usage() # type: ignore[arg-type] + assert rec.requests[0]["timeout"] == self._AS_DICT + + @pytest.mark.parametrize("bad", [(5, 0, 5, 5), (5, math.nan, 5, 5), (5, -1, 5, 5), (5, True, 5, 5), (5, 5), ()]) + def test_a_bad_tuple_is_refused(self, bad: Any) -> None: + with pytest.raises(ValueError, match="timeout"): + Lenz(api_key=API_KEY, timeout=bad) + with pytest.raises(ValueError, match="timeout"): + Lenz(api_key=API_KEY).usage(timeout=bad) + + def test_a_tuple_with_an_unbounded_part(self) -> None: + with recorded(_USAGE) as rec: + Lenz(api_key=API_KEY).usage(timeout=(5, None, 5, 5)) # type: ignore[arg-type] + assert rec.requests[0]["timeout"]["read"] is None + + def test_any_real_number_and_any_integer_like_retry_count(self) -> None: + from fractions import Fraction + + class Three: + def __index__(self) -> int: + return 3 + + client = Lenz(api_key=API_KEY, timeout=Fraction(5), max_retries=Three()) # type: ignore[arg-type] + client.with_options(timeout=Fraction(1, 2), max_retries=Three()) # type: ignore[arg-type] + with recorded({("GET", "/me/usage"): [S503]}) as rec, pytest.raises(lenz_io.LenzAPIError): + client.usage(max_retries=Three(), timeout=7) # type: ignore[arg-type] + assert len(rec.requests) == 4 + + +class TestHeaderSyntax: + @pytest.mark.parametrize( + "headers", + [ + {"Bad Name": "x"}, + {"X:A": "x"}, + {"X-Ä": "x"}, + {"X-A\n": "x"}, + {"X-A": "a\r\nInjected: 1"}, + {"X-A": "a\x00"}, + {"X-A": "café"}, + ], + ) + def test_refused_before_a_key_or_a_request(self, headers: dict[str, str], no_key_minted: None) -> None: + client = Lenz(api_key=API_KEY) + with httpx_refused(): + for call in ( + lambda: client.verify("A.", extra_headers=headers), + lambda: client.review_and_wait(DRAFT, extra_headers=headers), + lambda: client.with_options(extra_headers=headers), + ): + with pytest.raises(ValueError, match="header"): + call() + + def test_tab_and_space_are_allowed_in_a_value(self) -> None: + with recorded(_USAGE) as rec: + Lenz(api_key=API_KEY).usage(extra_headers={"X-A": "a b\tc"}) + assert _headers(rec.requests[0])["x-a"] == "a b\tc" + + def test_a_poll_that_cannot_be_sent_is_not_read_as_an_unreadable_body( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + # An error raised while building a poll request (before anything is + # sent) ends the wait at once instead of being polled to the timeout. + real = Lenz._request + + def failing(self: Lenz, method: str, path: str, **kw: Any) -> Any: + if method == "GET": + raise UnicodeEncodeError("ascii", "é", 0, 1, "not ASCII") + return real(self, method, path, **kw) + + monkeypatch.setattr(Lenz, "_request", failing) + with recorded(_REVIEW_WAIT) as rec, pytest.raises(UnicodeEncodeError): + Lenz(api_key=API_KEY).review_and_wait(DRAFT, timeout=60) + assert rec.clock.sleeps == [] + + +def test_a_with_block_on_a_subclass_copy_keeps_the_subclass() -> None: + class Sub(Lenz): + pass + + with Sub(api_key=API_KEY).with_options(timeout=5) as c: + assert isinstance(c, Sub) + ann = inspect.signature(Lenz.__enter__).return_annotation + assert "Self" in str(ann) From 0133e4cf8fa7d530d52b0f6d1a8097aea20a0899 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sat, 10 Oct 2026 00:05:35 +0300 Subject: [PATCH 60/67] docs: accepted timeout forms, header syntax, shallow copies, private plumbing Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 25 ++++++++++++++++--------- README.md | 15 ++++++++++----- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d0c39b..1b3f5af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,16 +82,23 @@ Deprecated). ### Changed - **One rule for every timeout and retry count of a request, checked before - anything is sent.** A timeout must be a finite number of seconds greater - than 0, `None` (no timeout) or an `httpx.Timeout`; a retry count a whole - number, 0 or more (`True` / `False` are refused). Anything else raises - `ValueError`: in `Lenz(timeout=..., max_retries=...)` when the client is - built, and in the `timeout=` of `extract` / `assess` before the call mints a - key or sends a request. These values did not work before (a zero or - negative timeout failed every request, a negative retry count sent none), - except `True` / `False`, which were read as 1 / 0: pass the number. - The wait helpers' `timeout` (how long to wait) is not affected: `0` or less + anything is sent.** A timeout must be `None` (no timeout), a finite real + number of seconds greater than 0, httpx's `(connect, read, write, pool)` + tuple of such numbers or `None`, or an `httpx.Timeout`; a retry count a + whole number, 0 or more. Anything else raises `ValueError`: in + `Lenz(timeout=..., max_retries=...)` when the client is built, and in the + `timeout=` of `extract` / `assess` before the call mints a key or sends a + request. Newly refused: a timeout of 0 or less, NaN or infinity, a negative + retry count (none of these worked: such a timeout failed every request, a + negative retry count sent none), and `True` / `False`, which were read as + 1 / 0 (pass the number). Real numbers of any type (`Fraction`, numpy + scalars), the tuple form and integer-like retry counts keep working. The + wait helpers' `timeout` (how long to wait) is not affected: `0` or less still reads once. +- **The private `_request` / `_send` methods take other arguments.** Code that + overrode them to add headers or change timeouts should use the + `extra_headers` / `timeout` / `max_retries` options or `with_options` + instead. - **An id goes into the URL path as one segment.** Every method that puts an id in a path (`get_status`, `select`, `get_review`, `get_citecheck`, the cancels, `verifications.*`, `ask.*`, the waits) percent-encodes it whole, so diff --git a/README.md b/README.md index f5e517d..b834fe6 100644 --- a/README.md +++ b/README.md @@ -1231,9 +1231,10 @@ Environment variables: An OAuth access token for the Lenz API works wherever the API key goes: pass it as `api_key` or in `LENZ_API_KEY`. -`timeout` must be a number of seconds greater than 0, `None` (no timeout) or an -`httpx.Timeout`; `max_retries` a whole number, 0 or more. Anything else raises -`ValueError` when the client is built. +`timeout` must be a number of seconds greater than 0, `None` (no timeout), an +`httpx.Timeout` or httpx's `(connect, read, write, pool)` tuple; `max_retries` a +whole number, 0 or more. Anything else raises `ValueError` when the client is +built. ### Per-call options @@ -1251,7 +1252,9 @@ client.verify("...", extra_headers={"X-Trace-Id": trace}) # added to the reques whole call, and each retry gets its own. - `max_retries`: how often a request that failed in a way worth retrying (a 5xx, a 429, a dropped connection) is sent again: a whole number, 0 or more. -- `extra_headers`: headers added to the request. The SDK's own headers are +- `extra_headers`: headers added to the request. Names are header tokens and + values visible ASCII (spaces and tabs allowed); anything else raises + `ValueError`. The SDK's own headers are refused (`X-Lenz-API-Version`, `Idempotency-Key`, `Authorization`, `Content-Type`, `Content-Length`, `Host`, `Transfer-Encoding`): use `idempotency_key=` and `api_key=` for the first two. A header with the name @@ -1306,7 +1309,9 @@ A copy of a copy starts from the copy's options. The pool belongs to the client that created it: `close()` and `with` on a copy do nothing, closing the original closes the pool for every copy (a copy then raises httpx's closed-client error), and a client given `http_client=` never closes it. A -copy is as safe to share across threads as the client. +copy is as safe to share across threads as the client. The copy is shallow: +attributes a subclass of `Lenz` adds are shared with the client it was made +from. ## Compatibility From 48e3a9f0e9750038e5eb276c43e8ab2635ca3874 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sat, 10 Oct 2026 00:09:59 +0300 Subject: [PATCH 61/67] fix: refuse header values with whitespace at either end A value starting or ending with a space or tab passed the check, minted a key and then failed in httpx. It is now a ValueError before anything is minted or sent; spaces and tabs inside a value and an empty value stay allowed. README: the with_options row says a copy's max_retries never reaches a wait's polls, an option User-Agent/Accept replaces the default in any casing, and what a copy of a copy starts from. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- README.md | 12 +++++++----- src/lenz_io/client.py | 9 +++++---- tests/test_request_options.py | 15 ++++++++++++++- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b834fe6..567d29b 100644 --- a/README.md +++ b/README.md @@ -1253,12 +1253,13 @@ client.verify("...", extra_headers={"X-Trace-Id": trace}) # added to the reques - `max_retries`: how often a request that failed in a way worth retrying (a 5xx, a 429, a dropped connection) is sent again: a whole number, 0 or more. - `extra_headers`: headers added to the request. Names are header tokens and - values visible ASCII (spaces and tabs allowed); anything else raises - `ValueError`. The SDK's own headers are + values visible ASCII, with spaces and tabs allowed inside but not at either + end (an empty value is fine); anything else raises `ValueError`. The SDK's own headers are refused (`X-Lenz-API-Version`, `Idempotency-Key`, `Authorization`, `Content-Type`, `Content-Length`, `Host`, `Transfer-Encoding`): use `idempotency_key=` and `api_key=` for the first two. A header with the name - of a default one (`User-Agent`, `Accept`) replaces it. + of a default one (`User-Agent`, `Accept`), in any casing, replaces the + default instead of being sent next to it. What each option reaches: @@ -1268,7 +1269,7 @@ What each option reaches: | `extract`, `assess` | the attempt, used as given | the call's retries | every request | | Waits (`wait`, `verify_and_wait`, `verify_batch_and_wait`, `review_and_wait`, `citecheck_and_wait`) | **how long to wait** (unchanged) | the submit's retries (`wait` has none: each poll is one request) | the submit and every poll | | `verifications.iter`, `library.iter` | each page's attempt | each page's retries | every page | -| `with_options` | the default attempt timeout of the copy (also what each poll of a wait uses, capped by what is left of the wait) | the copy's default | added to every request of the copy | +| `with_options` | the default attempt timeout of the copy (also what each poll of a wait uses, capped by what is left of the wait) | the copy's default for plain calls and submits; never a wait's polls, which are one attempt each | added to every request of the copy | A bad value raises `ValueError` before anything is sent (for the iterators, when the iterator is created). Precedence, per option: the call's keyword, then @@ -1305,7 +1306,8 @@ for job in jobs: scoped.review_and_wait(job.draft) ``` -A copy of a copy starts from the copy's options. The pool belongs to the +A copy of a copy starts from the copy's options (timeout, retries and +headers), and changes only what it is given. The pool belongs to the client that created it: `close()` and `with` on a copy do nothing, closing the original closes the pool for every copy (a copy then raises httpx's closed-client error), and a client given `http_client=` never closes it. A diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index c0f5c0f..4c2ed52 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -297,9 +297,10 @@ def _check_retries(value: Any, where: str) -> int: #: A header name is an RFC 7230 token; a value is visible ASCII, spaces and -#: tabs (no CR, LF or NUL: those would end the header or the request). +#: tabs (no CR, LF or NUL: those would end the header or the request), or empty. _HEADER_NAME = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+") -_HEADER_VALUE = re.compile(r"[\t\x20-\x7e]*") +#: Spaces and tabs only inside the value: at either end httpx refuses it. +_HEADER_VALUE = re.compile(r"(?:[\x21-\x7e](?:[\t\x20-\x7e]*[\x21-\x7e])?)?") def _check_headers(value: Any, where: str) -> tuple[tuple[str, str | None], ...]: @@ -323,8 +324,8 @@ def _check_headers(value: Any, where: str) -> tuple[tuple[str, str | None], ...] raise ValueError(f"{where}: the {name} header is set by the SDK and cannot be passed in extra_headers.") if header is not None and (not isinstance(header, str) or not _HEADER_VALUE.fullmatch(header)): raise ValueError( - f"{where}: the value of header {name} must be a string of visible ASCII characters, spaces " - f"and tabs, or None." + f"{where}: the value of header {name} must be a string of visible ASCII characters, with " + f"spaces and tabs only between them (not at either end), or None." ) pairs.append((name, header)) return tuple(pairs) diff --git a/tests/test_request_options.py b/tests/test_request_options.py index 05673b4..5e2b808 100644 --- a/tests/test_request_options.py +++ b/tests/test_request_options.py @@ -1027,8 +1027,21 @@ def test_refused_before_a_key_or_a_request(self, headers: dict[str, str], no_key def test_tab_and_space_are_allowed_in_a_value(self) -> None: with recorded(_USAGE) as rec: - Lenz(api_key=API_KEY).usage(extra_headers={"X-A": "a b\tc"}) + Lenz(api_key=API_KEY).usage(extra_headers={"X-A": "a b\tc", "X-Empty": ""}) assert _headers(rec.requests[0])["x-a"] == "a b\tc" + assert _headers(rec.requests[0])["x-empty"] == "" + + @pytest.mark.parametrize("value", ["trace ", " trace", "\ttrace", "trace\t", " ", "\t"]) + def test_whitespace_at_either_end_of_a_value_is_refused(self, value: str, no_key_minted: None) -> None: + client = Lenz(api_key=API_KEY) + with httpx_refused(): + for call in ( + lambda: client.verify("A.", extra_headers={"X-A": value}), + lambda: client.usage(extra_headers={"X-A": value}), + lambda: client.with_options(extra_headers={"X-A": value}), + ): + with pytest.raises(ValueError, match="header"): + call() def test_a_poll_that_cannot_be_sent_is_not_read_as_an_unreadable_body( self, monkeypatch: pytest.MonkeyPatch From b4875a0da37f5c03d2ad53a93f1e377979db15f3 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sat, 10 Oct 2026 00:49:11 +0300 Subject: [PATCH 62/67] fix: final 3.0 review: empty webhook secret, timeout limit, infinite Retry-After, 2.x blank-claim wording, cancel errors as sent - verify_signature raises ValueError on an empty secret, as LenzWebhooks(secret="") does. - A timeout is at most 2,147,483 s (Node's limit); a longer one overflowed on every request. Refused with ValueError naming the limit. - An infinite Retry-After is read as the longest wait instead of raising OverflowError; an infinite retry_after in an error body reads None. - A blank claim reads the 2.x sentence again ("Text is required.", the batch item's, select's), as the Node SDK rebuilds it. - A cancel's error body is read as sent (a schema 422 keeps validation_error), as in Node. - Docs: upgrade must-do for cancelled in polling loops; whitespace-only webhook_url and chain_id in model_dump under Changed; the exact 2.x version-header story; cancels retry on 429; 410 ends a wait; VerificationCancelled.verification; the failed-row idiom in the first call; BatchItemResult and smoke docstrings corrected. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 53 +++++++++++---- README.md | 17 +++-- src/lenz_io/client.py | 33 ++++++++-- src/lenz_io/errors.py | 24 ++++++- src/lenz_io/models.py | 6 +- src/lenz_io/webhooks.py | 5 ++ tests/test_release_edges.py | 126 ++++++++++++++++++++++++++++++++++++ tests/test_smoke_staging.py | 16 ++--- 8 files changed, 243 insertions(+), 37 deletions(-) create mode 100644 tests/test_release_edges.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3f5af..32203d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,10 @@ All notable changes to this SDK are documented here. Format follows > shape; (3) re-record tests that replay recorded 2.x response bodies; (4) an > idempotent request first sent before the switch and replayed after it raises > `LenzApiVersionError`: finish that work with 2.x, never change the key to -> get past it. May do: move off the deprecated names (they keep working). +> get past it; (5) a hand-written polling loop must treat `cancelled` as +> final: `get_status`, `get_review` and `get_citecheck` return it where 2.21 +> returned `failed`, so a loop waiting for `failed` polls until its own +> timeout. May do: move off the deprecated names (they keep working). > Details under Migration. Major release (3.0.0). The SDK asks for the API's current response shape @@ -81,14 +84,37 @@ Deprecated). ### Changed +- **`verify_signature` refuses an empty secret** with `ValueError`, as + `LenzWebhooks(secret="")` always did. 2.x accepted a body signed with the + empty key. +- **A whitespace-only `webhook_url` is no longer sent** on `verify`, + `verify_batch` (the batch-wide value and each item's) and the `*_and_wait` + helpers that submit them; 2.x sent it as given. It means the key's default + webhook either way, but the request body (and so its idempotency hash) + differs: a request first sent by 2.x with a pinned `Idempotency-Key` and a + whitespace-only `webhook_url`, and resent by 3.0 with the same key, is + refused with a 422 (`idempotency_body_mismatch`). Finish such a request + with 2.x, or resend it with a new key. +- **`TaskAccepted.model_dump()` has no `chain_id` key**: 2.x carried it only + because the API sent it, and the API no longer does. The + `TaskAccepted.chain_id` attribute is kept and reads `""` (deprecated). +- **A `Retry-After` the SDK cannot use is read safely.** An infinite wait + (`Retry-After: 1e999`) is read as the longest wait, past every cap (a 429 + then raises at once), where it raised `OverflowError`; on the error's + `retry_after` it reads `None` (unknown). +- **A cancel's error body is read as sent**: its `code` (a 422's + `validation_error` too), `detail` and `errors`. - **One rule for every timeout and retry count of a request, checked before anything is sent.** A timeout must be `None` (no timeout), a finite real number of seconds greater than 0, httpx's `(connect, read, write, pool)` tuple of such numbers or `None`, or an `httpx.Timeout`; a retry count a - whole number, 0 or more. Anything else raises `ValueError`: in + whole number, 0 or more. A timeout is at most 2,147,483 seconds (about 24.8 + days, the Node SDK's limit): a longer one overflowed in the socket layer on + every request. Anything else raises `ValueError`: in `Lenz(timeout=..., max_retries=...)` when the client is built, and in the `timeout=` of `extract` / `assess` before the call mints a key or sends a - request. Newly refused: a timeout of 0 or less, NaN or infinity, a negative + request. Newly refused: a timeout of 0 or less, NaN, infinity or past the + limit, a negative retry count (none of these worked: such a timeout failed every request, a negative retry count sent none), and `True` / `False`, which were read as 1 / 0 (pass the number). Real numbers of any type (`Fraction`, numpy @@ -107,8 +133,10 @@ Deprecated). and `".."` raise `ValueError` before any request (the methods that took an empty id now raise as `get_review` and `wait` already did). - **The SDK asks for the API's current response shape, and reads only that.** Every request sends - `X-Lenz-API-Version: 2026-10-11` (`lenz_io.API_VERSION`; 2.x sent - `2026-05-13`). In that shape each field, status and error code has one name + `X-Lenz-API-Version: 2026-10-11` (`lenz_io.API_VERSION`). 2.x sent + `2026-05-13` from the client it built, and no version header at all through + an `http_client=` of your own (the server then answered in the account's + version); 3.0 sends it on every request either way. In that shape each field, status and error code has one name across every endpoint. - **Every attribute keeps the value it had in 2.x** (except the `status` of a cancelled task, see Breaking), computed from the @@ -243,8 +271,9 @@ Deprecated). deep checks that finished, citations checked) and the rest is refunded or never charged. A `wait` on a cancelled run raises the failed error with `failure_class == "cancelled"`. Needs the API to serve version `2026-10-11`. -- **`VerificationCancelled`** (new webhook event class, `verification.cancelled`), - and `review.cancelled` / `citecheck.cancelled` typed as `ReviewEvent` / +- **`VerificationCancelled`** (new webhook event class, `verification.cancelled`; + its `verification` property reads the payload as `get_status` returns it, + `status` `"cancelled"`), and `review.cancelled` / `citecheck.cancelled` typed as `ReviewEvent` / `CitecheckEvent`. They are sent only for work submitted with `2026-10-11`; a cancellation of older work keeps arriving as `*.failed`. See Breaking. - `LenzNotFoundError` (404), `LenzConnectionError` and @@ -431,10 +460,12 @@ rebuild: - `model_dump()` and the CLI's `--json` return the 2.x-compatible fields (computed from the response) plus the current-shape keys the server sent; they are not the wire body. If a script parses `--json` output, it keeps - finding every 2.x key and gains the new ones. -- `webhook_url` keeps its meaning: on `verify` and `verify_batch` an empty - or whitespace-only value (the default is `""`) means your key's default - webhook and is not sent; on `review` and `citecheck`, `None` means your + finding every 2.x key but `chain_id` (see Changed) and gains the new ones. +- `webhook_url` keeps its meaning, though not always its bytes: on `verify` + and `verify_batch` an empty or whitespace-only value (the default is `""`) + means your key's default webhook and is not sent (2.x sent a + whitespace-only one; see Changed for its effect on a pinned + `Idempotency-Key`); on `review` and `citecheck`, `None` means your key's default and `""` means no webhook. In the current shape the API reads a missing `webhook_url` as the key's default and `""` as no webhook on every endpoint. diff --git a/README.md b/README.md index 567d29b..122fa25 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ from lenz_io import Lenz client = Lenz() # reads LENZ_API_KEY row = client.assess(claim="The Great Wall of China is visible from space.").claims[0] -print(row.verdict, row.confidence) # e.g. False high (about 15-20 s, 1 credit) +if row.status == "failed": # no verdict for this row (not charged): failure says why + print(row.failure.code, row.failure.hint) +else: + print(row.verdict, row.confidence) # e.g. False high (about 15-20 s, 1 credit) ``` From there: [the whole ladder](#quickstart--the-canonical-integration) on an @@ -871,9 +874,9 @@ Local argument mistakes (an empty id, two exclusive arguments) raise A `*_and_wait` helper that reaches its own `timeout` raises `LenzTimeoutError` (`ReviewTimeout`, `CitecheckTimeout`): the job keeps running server-side, so -read it later by its id rather than resubmitting. A poll answered 401, 403 or -404 (or in another API version) ends the wait at once with that error; in -`verify_batch_and_wait` a 404 or a version error fails that item only (the +read it later by its id rather than resubmitting. A poll answered 401, 403, +404 or 410 (or in another API version) ends the wait at once with that error; +in `verify_batch_and_wait` a 404, a 410 or a version error fails that item only (the others keep going), while a 401 / 403 raises. A 5xx, a 429 or a network failure is polled again. Each poll is bounded by what is left of the `timeout` (each phase at most the client's own), and no poll starts once it @@ -994,8 +997,8 @@ print(check.status, check.credits.charged) the rest is refunded or never charged. `credits.charged` on the result is what the review or check cost. - Cancelling twice is safe, so the calls send no `Idempotency-Key`. They are - retried on a 5xx or a dropped connection, like any call that is safe to - repeat. + retried on a 5xx, a 429 or a dropped connection, like any call that is safe + to repeat. - Afterwards `get_status`, `get_review` and `get_citecheck` return the `cancelled` status, and `wait`, `review_and_wait` and `citecheck_and_wait` raise the failed error with `failure_class == "cancelled"`. A `webhook_url` @@ -1231,7 +1234,7 @@ Environment variables: An OAuth access token for the Lenz API works wherever the API key goes: pass it as `api_key` or in `LENZ_API_KEY`. -`timeout` must be a number of seconds greater than 0, `None` (no timeout), an +`timeout` must be a number of seconds greater than 0 and at most 2,147,483, `None` (no timeout), an `httpx.Timeout` or httpx's `(connect, read, write, pool)` tuple; `max_retries` a whole number, 0 or more. Anything else raises `ValueError` when the client is built. diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 4c2ed52..7a55db5 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -253,9 +253,21 @@ def __deepcopy__(self, memo: Any) -> NotGiven: ) +#: The longest timeout a request can take, in seconds (the Node SDK's limit, +#: 2^31 - 1 ms): past it the socket layer overflows on every request. Also the +#: longest wait a ``Retry-After`` is read as. +MAX_TIMEOUT_SECONDS = 2_147_483 + + def _seconds(value: Any) -> bool: - """A finite real number of seconds greater than 0 (``bool`` is not one).""" - return not isinstance(value, bool) and isinstance(value, numbers.Real) and math.isfinite(value) and float(value) > 0 + """A finite real number of seconds greater than 0 and at most + :data:`MAX_TIMEOUT_SECONDS` (``bool`` is not one).""" + return ( + not isinstance(value, bool) + and isinstance(value, numbers.Real) + and math.isfinite(value) + and 0 < float(value) <= MAX_TIMEOUT_SECONDS + ) def _snapshot(value: Any) -> Any: @@ -273,12 +285,14 @@ def _check_timeout(value: Any, where: str) -> float | httpx.Timeout | None: if value is None or _seconds(value): return value # type: ignore[no-any-return] if isinstance(value, httpx.Timeout): - return httpx.Timeout(value) - if isinstance(value, tuple) and len(value) == 4 and all(part is None or _seconds(part) for part in value): + parts = (value.connect, value.read, value.write, value.pool) + if not any(isinstance(p, (int, float)) and p > MAX_TIMEOUT_SECONDS for p in parts): + return httpx.Timeout(value) + elif isinstance(value, tuple) and len(value) == 4 and all(part is None or _seconds(part) for part in value): return httpx.Timeout(value) raise ValueError( - f"{where}: timeout must be a number of seconds greater than 0, None, an httpx.Timeout or a " - f"(connect, read, write, pool) tuple of such numbers or None (got {value!r})." + f"{where}: timeout must be a number of seconds greater than 0 and at most 2,147,483, None, an " + f"httpx.Timeout or a (connect, read, write, pool) tuple of such numbers or None (got {value!r})." ) @@ -3223,7 +3237,12 @@ def _stated_retry_after(response: httpx.Response) -> int | None: # Floored at zero: `Retry-After: -5` is malformed, and time.sleep() # raises ValueError on a negative — which would escape the retry # ladder as a bare ValueError, defeating the typed-exception contract. - return max(0, int(float(raw))) + seconds = float(raw) + if math.isnan(seconds): + return None + # ``Retry-After: 1e999`` is infinity: read as the longest wait, which + # is past any cap (so a 429 raises), never an OverflowError. + return int(min(max(seconds, 0.0), MAX_TIMEOUT_SECONDS)) except (TypeError, ValueError): return None diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index 88017f3..c9cb0e8 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -673,6 +673,7 @@ def _parse_body(raw: bytes | str | None) -> dict[str, Any]: #: The cancel calls are new in 3.0: no earlier shape to read, so the server's #: ``code`` stays on the error (``not_found``, ``use_review_cancel``, ...). _CANCEL_PATH = re.compile(r"^/(?:verify|reviews|citechecks)/[^/]+/cancel$") +_SELECT_PATH = re.compile(r"^/verify/[^/]+/select$") #: Codes the newer shape sends where the original error carried no ``code`` #: (outside /review and /citecheck, which always sent one). @@ -719,6 +720,11 @@ def _original_error(status: int, parsed: dict[str, Any], method: str, path: str) code = out["code"] if isinstance(out.get("code"), str) else "" errors = out["errors"] if isinstance(out.get("errors"), list) else None path = path.split("?", 1)[0] + if method.upper() == "POST" and _CANCEL_PATH.match(path): + # The three cancel calls are new in 3.0: there is no 2.x reading of + # their errors, so the body is read as sent (code, detail and errors), + # as the Node SDK reads it. + return out if _REVIEW_FAMILY.match(path): # A missing or unknown credential is refused before the endpoint runs. if code == "not_authenticated": @@ -762,6 +768,20 @@ def _original_error(status: int, parsed: dict[str, Any], method: str, path: str) out["credits_remaining"] = out["remaining"] _original_wait_and_link(out, status) return out + if status == 422 and code == "blank_input" and isinstance(out.get("detail"), str) and method.upper() == "POST": + # A blank input said "Text is required." (or named its item, or + # ``texts``), where the newer sentence names ``claim`` / ``claims``. + loc = errors[0].get("loc") if errors and isinstance(errors[0], dict) else None + if isinstance(loc, list) and loc and loc[0] == "body": + if path in ("/verify", "/assess"): + if len(loc) == 2 and loc[1] == "claim": + out["detail"] = "Text is required." + elif path == "/verify/batch": + if len(loc) == 4 and loc[1] == "claims" and isinstance(loc[2], int) and not isinstance(loc[2], bool): + out["detail"] = f"claims[{loc[2]}].text is required." + elif _SELECT_PATH.match(path): + if len(loc) == 2 and loc[1] == "claims": + out["detail"] = "texts is required and must be non-empty." if status == 422 and code == "blank_input" and path == "/assess": loc = errors[0].get("loc") if errors and isinstance(errors[0], dict) else None if isinstance(loc, list) and "claims" in loc: @@ -799,7 +819,6 @@ def _original_error(status: int, parsed: dict[str, Any], method: str, path: str) codeless = (code in _CODELESS and not (code == "too_many_items" and path == "/assess")) or ( code == "verification_not_ready" and path.startswith("/ask/") ) - codeless = codeless and not _CANCEL_PATH.match(path) if codeless: del out["code"] out.pop("errors", None) @@ -1038,7 +1057,8 @@ def _opt_int(value: Any) -> int | None: return None try: return int(float(value)) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): + # OverflowError: an infinite value (``"1e999"``) is unknown, as in Node. return None diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 4008775..5a87310 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -362,7 +362,7 @@ class Verification(_Lax): The verdict block is FLAT at top level (was nested ``Verdict`` object pre-unify). ``created_at`` + ``completed_at`` are the only timestamp fields on the API surface (``modified_at`` is the deprecated 2.x name of - the second) — editorial ``published_at`` is internal-only. + the second); a claim's ``published_at`` is not part of the API. 1.1.0: dropped ``url`` and ``visibility``. API claims are private by default and referenced by ``verification_id`` only. Cache-hit on @@ -1091,7 +1091,9 @@ class BatchItemResult(_Lax): - ``needs_input`` — paused for caller input; inspect ``status_detail`` (reason / claims). - ``failed`` — terminal failure (or completed-without-result), or cancelled elsewhere (``status_detail.status`` is then ``cancelled``); ``status_detail`` carries the diagnostic. - A verification removed by its account's retention period (HTTP 410) is ``failed``, ``status_detail`` ``None``. + An item whose poll could never succeed is ``failed`` with ``status_detail`` ``None``: a verification + removed by its account's retention period (HTTP 410), a task id nothing was found under (404), or an + answer in another API version. - ``timeout`` — the deadline elapsed before this task reached a terminal state; ``status_detail`` is ``None``. """ diff --git a/src/lenz_io/webhooks.py b/src/lenz_io/webhooks.py index 5b51aee..2cc5f5d 100644 --- a/src/lenz_io/webhooks.py +++ b/src/lenz_io/webhooks.py @@ -72,7 +72,12 @@ def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool: Returns rather than raising on success makes ``if verify_signature(...)`` idioms work; the raise-on-bad path means a silent ``False`` can't accidentally pass through. + + An empty ``secret`` raises ``ValueError``, as ``LenzWebhooks(secret="")`` + does: a body signed with the empty key proves nothing. """ + if not secret: + raise ValueError("verify_signature requires a non-empty secret. Get it from /api-credentials.") if not signature: raise LenzWebhookSignatureError( message="Missing webhook signature", diff --git a/tests/test_release_edges.py b/tests/test_release_edges.py new file mode 100644 index 0000000..c43cd44 --- /dev/null +++ b/tests/test_release_edges.py @@ -0,0 +1,126 @@ +"""Edge inputs found in the final 3.0 reviews: an empty webhook secret, a +timeout too large for a socket, an infinite stated wait, the 2.x wording of +a blank claim, and a cancel's 422 keeping the server's code.""" + +from __future__ import annotations + +import json +from typing import Any + +import httpx +import pytest +import respx + +from lenz_io import Lenz, LenzRateLimitError, LenzWebhookSignatureError, verify_signature +from lenz_io.client import DEFAULT_BASE_URL +from lenz_io.errors import map_response_to_error +from lenz_io.webhooks import _sign + +KEY = "lenz_" + "0" * 32 + + +# ── 1. a webhook secret is never empty ───────────────────────────────────── + + +@pytest.mark.parametrize("secret", ["", None]) +def test_verify_signature_refuses_an_empty_secret(secret: Any) -> None: + body = b'{"event": "verification.completed"}' + with pytest.raises(ValueError, match="non-empty secret"): + verify_signature(body, _sign(body, ""), secret) + + +def test_a_real_secret_still_verifies() -> None: + body = b"{}" + assert verify_signature(body, _sign(body, "s3cret"), "s3cret") is True + with pytest.raises(LenzWebhookSignatureError): + verify_signature(body, _sign(body, "other"), "s3cret") + + +# ── 5. a timeout a socket can take ───────────────────────────────────────── + +_TOO_LONG = [1e10, 2_147_484, (5, 1e10, 5, 5)] + + +@pytest.mark.parametrize("bad", _TOO_LONG) +def test_a_timeout_past_the_limit_is_refused_everywhere(bad: Any) -> None: + for call in ( + lambda: Lenz(api_key=KEY, timeout=bad), + lambda: Lenz(api_key=KEY).with_options(timeout=bad), + lambda: Lenz(api_key=KEY).usage(timeout=bad), + lambda: Lenz(api_key=KEY).extract(text="Doc.", timeout=bad), + ): + with pytest.raises(ValueError, match="2,147,483"): + call() + + +def test_an_httpx_timeout_past_the_limit_is_refused() -> None: + with pytest.raises(ValueError, match="2,147,483"): + Lenz(api_key=KEY, timeout=httpx.Timeout(5, read=1e10)) + + +def test_the_limit_itself_is_accepted() -> None: + Lenz(api_key=KEY, timeout=2_147_483).with_options(timeout=(1, 2_147_483, None, 1)).close() + + +# ── 6. an infinite stated wait ───────────────────────────────────────────── + + +@pytest.mark.parametrize("header", ["1e999", "inf", "-1e999"]) +def test_an_infinite_retry_after_is_a_long_wait_not_a_crash(header: str, monkeypatch: pytest.MonkeyPatch) -> None: + slept: list[float] = [] + monkeypatch.setattr("lenz_io.client.time.sleep", slept.append) + with respx.mock(base_url=DEFAULT_BASE_URL) as r: + route = r.get("/me/usage").respond( + 429, json={"code": "rate_limited", "detail": "slow"}, headers={"Retry-After": header} + ) + with pytest.raises(LenzRateLimitError): + Lenz(api_key=KEY).usage() + # A wait past the cap on a 429 raises at once (a negative one is zero and retried). + assert route.call_count == (4 if header.startswith("-") else 1) + + +def test_an_infinite_retry_after_in_an_error_body_reads_as_unknown() -> None: + err = map_response_to_error( + 503, json.dumps({"code": "capacity", "detail": "busy", "retry_after": "1e999"}).encode(), {} + ) + assert err.retry_after is None + + +# ── 7. a blank claim reads as 2.x worded it ──────────────────────────────── + + +def _blank(loc: list[Any], detail: str) -> bytes: + return json.dumps( + {"detail": detail, "code": "blank_input", "errors": [{"loc": loc, "msg": detail, "type": "blank_input"}]} + ).encode() + + +@pytest.mark.parametrize( + ("method", "path", "loc", "expected"), + [ + ("POST", "/verify", ["body", "claim"], "Text is required."), + ("POST", "/assess", ["body", "claim"], "Text is required."), + ("POST", "/verify/batch", ["body", "claims", 1, "claim"], "claims[1].text is required."), + ("POST", "/verify/t1/select", ["body", "claims"], "texts is required and must be non-empty."), + ], +) +def test_a_blank_claim_keeps_the_2x_sentence(method: str, path: str, loc: list[Any], expected: str) -> None: + err = map_response_to_error(422, _blank(loc, "claim is required."), {}, endpoint=(method, path)) + assert err.message == expected + + +def test_another_blank_field_keeps_the_servers_sentence() -> None: + err = map_response_to_error(422, _blank(["body", "text"], "text is required."), {}, endpoint=("POST", "/extract")) + assert err.message == "text is required." + + +# ── 11. a cancel's 422 keeps the server's code ───────────────────────────── + + +@pytest.mark.parametrize("path", ["/verify/t1/cancel", "/reviews/r1/cancel", "/citechecks/c1/cancel"]) +def test_a_cancel_schema_422_keeps_its_code_and_errors(path: str) -> None: + errors = [{"type": "missing", "loc": ["body", "x"], "msg": "Field required"}] + body = {"detail": "Field required", "code": "validation_error", "errors": errors} + err = map_response_to_error(422, json.dumps(body).encode(), {}, endpoint=("POST", path)) + assert err.code == "validation_error" + assert err.message == "Field required" diff --git a/tests/test_smoke_staging.py b/tests/test_smoke_staging.py index a32e47c..6678513 100644 --- a/tests/test_smoke_staging.py +++ b/tests/test_smoke_staging.py @@ -4,14 +4,13 @@ release workflow invokes `pytest -m smoke` with `LENZ_E2E_KEY` set; tests are skipped if the env var is absent. -These exercise the SDK against the live API across the four primitives: +These exercise the SDK against the live API: 1. ``extract`` — free, parses identified_claims 2. ``assess`` — fast 3-model verdict, returns flat claim entries 3. ``verify_and_wait`` — the quickstart claim at ``depth="low"``, the cheap run (~60s); a cache hit is a bonus, never assumed - 4. ``ask.history`` — read-only follow-up surface (no exchange burned) - 5. ``cancel`` — a ``depth="low"`` run stopped right after it starts - 6. request options — ``with_options`` and a per-call ``timeout`` on ``assess`` + 4. ``cancel`` — a ``depth="low"`` run stopped right after it starts + 5. request options — ``with_options`` and a per-call ``timeout`` on ``assess`` Plus webhook signature roundtrip + ``/me/usage`` shape. @@ -63,9 +62,10 @@ def test_a_run_can_be_cancelled(smoke_client): """``cancel`` answers 200 whatever the state of the run: stopped by this call, or already ended. Neither is an error, so the test cannot be flaky. - A cancelled verification is not charged. A cache hit (an answer the API - already holds) has finished by the time we cancel, which is the other - branch. Not the quickstart claim, so it is less likely to be one.""" + A cancelled verification is not charged. A run the API answers from its + cache still goes through the pipeline, so the cancel may stop it or find + it already completed; either branch passes. Not the quickstart claim, so + a cache hit is less likely.""" started = smoke_client.verify(claim="The Eiffel Tower is in Paris", depth="low") result = smoke_client.cancel(started.task_id) assert result.task_id == started.task_id @@ -95,7 +95,7 @@ def test_assess_returns_typed_claims(smoke_client): def test_request_options_reach_the_live_api(smoke_client): """The request options on a real call: a copy with its own retries and an extra header, and a per-call timeout (the same claim as above, so the - API's verdict memo usually answers it).""" + API's cache usually answers it).""" copy = smoke_client.with_options(max_retries=1, extra_headers={"X-Smoke-Check": "request-options"}) out = copy.assess(claim="Sharks don't get cancer", timeout=120) assert out.claims and out.claims[0].verdict From e63650d4556d8ca25f9ff681278b437091717da7 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sat, 10 Oct 2026 00:50:59 +0300 Subject: [PATCH 63/67] fix: a non-finite Retry-After is no stated wait, as in Node inf, -inf, nan and 1e999 now fall back to the normal backoff instead of being clamped to the longest wait; a huge finite value is still clamped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 9 +++++---- src/lenz_io/client.py | 9 +++++---- tests/test_release_edges.py | 20 ++++++++++++++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32203d2..dacdb0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,10 +98,11 @@ Deprecated). - **`TaskAccepted.model_dump()` has no `chain_id` key**: 2.x carried it only because the API sent it, and the API no longer does. The `TaskAccepted.chain_id` attribute is kept and reads `""` (deprecated). -- **A `Retry-After` the SDK cannot use is read safely.** An infinite wait - (`Retry-After: 1e999`) is read as the longest wait, past every cap (a 429 - then raises at once), where it raised `OverflowError`; on the error's - `retry_after` it reads `None` (unknown). +- **A `Retry-After` the SDK cannot use is read safely.** A non-finite value + (`inf`, `-inf`, `nan`, `1e999`) reads as no stated wait, so the normal + backoff runs, as in the Node SDK, where it raised `OverflowError`; on the + error's `retry_after` it reads `None` (unknown). A huge finite wait is + clamped to 2,147,483 seconds, still past every cap. - **A cancel's error body is read as sent**: its `code` (a 422's `validation_error` too), `detail` and `errors`. - **One rule for every timeout and retry count of a request, checked before diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index 7a55db5..f5e938c 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -255,7 +255,7 @@ def __deepcopy__(self, memo: Any) -> NotGiven: #: The longest timeout a request can take, in seconds (the Node SDK's limit, #: 2^31 - 1 ms): past it the socket layer overflows on every request. Also the -#: longest wait a ``Retry-After`` is read as. +#: longest finite wait a ``Retry-After`` is read as. MAX_TIMEOUT_SECONDS = 2_147_483 @@ -3238,10 +3238,11 @@ def _stated_retry_after(response: httpx.Response) -> int | None: # raises ValueError on a negative — which would escape the retry # ladder as a bare ValueError, defeating the typed-exception contract. seconds = float(raw) - if math.isnan(seconds): + if not math.isfinite(seconds): + # ``inf``, ``nan``, ``1e999``: no stated wait (the backoff ladder + # runs), as in the Node SDK; never an OverflowError. return None - # ``Retry-After: 1e999`` is infinity: read as the longest wait, which - # is past any cap (so a 429 raises), never an OverflowError. + # A huge finite wait is clamped (still past every cap). return int(min(max(seconds, 0.0), MAX_TIMEOUT_SECONDS)) except (TypeError, ValueError): return None diff --git a/tests/test_release_edges.py b/tests/test_release_edges.py index c43cd44..f6285a7 100644 --- a/tests/test_release_edges.py +++ b/tests/test_release_edges.py @@ -65,8 +65,8 @@ def test_the_limit_itself_is_accepted() -> None: # ── 6. an infinite stated wait ───────────────────────────────────────────── -@pytest.mark.parametrize("header", ["1e999", "inf", "-1e999"]) -def test_an_infinite_retry_after_is_a_long_wait_not_a_crash(header: str, monkeypatch: pytest.MonkeyPatch) -> None: +@pytest.mark.parametrize("header", ["1e999", "inf", "-inf", "-1e999", "nan"]) +def test_a_non_finite_retry_after_is_no_stated_wait(header: str, monkeypatch: pytest.MonkeyPatch) -> None: slept: list[float] = [] monkeypatch.setattr("lenz_io.client.time.sleep", slept.append) with respx.mock(base_url=DEFAULT_BASE_URL) as r: @@ -75,8 +75,20 @@ def test_an_infinite_retry_after_is_a_long_wait_not_a_crash(header: str, monkeyp ) with pytest.raises(LenzRateLimitError): Lenz(api_key=KEY).usage() - # A wait past the cap on a 429 raises at once (a negative one is zero and retried). - assert route.call_count == (4 if header.startswith("-") else 1) + # As in Node: no stated wait, so the normal backoff ladder runs. + assert route.call_count == 4 + assert slept == [1.0, 2.0, 4.0] + + +def test_a_huge_finite_retry_after_is_clamped_and_raises_at_once(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: None) + with respx.mock(base_url=DEFAULT_BASE_URL) as r: + route = r.get("/me/usage").respond( + 429, json={"code": "rate_limited", "detail": "slow"}, headers={"Retry-After": "1e300"} + ) + with pytest.raises(LenzRateLimitError): + Lenz(api_key=KEY).usage() + assert route.call_count == 1 def test_an_infinite_retry_after_in_an_error_body_reads_as_unknown() -> None: From 723c31bede286982a0f681f68f200320492b49b0 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sat, 10 Oct 2026 00:53:16 +0300 Subject: [PATCH 64/67] docs: failure block values read the server's, booleans refused, transport errors no longer httpx's Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dacdb0a..b8c8973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,8 +117,9 @@ Deprecated). request. Newly refused: a timeout of 0 or less, NaN, infinity or past the limit, a negative retry count (none of these worked: such a timeout failed every request, a - negative retry count sent none), and `True` / `False`, which were read as - 1 / 0 (pass the number). Real numbers of any type (`Fraction`, numpy + negative retry count sent none), and booleans: `True` / `False` as a + timeout or a retry count, which 2.21 read as 1 / 0 (`Lenz(timeout=True)` + was a 1-second timeout; pass the number). Real numbers of any type (`Fraction`, numpy scalars), the tuple form and integer-like retry counts keep working. The wait helpers' `timeout` (how long to wait) is not affected: `0` or less still reads once. @@ -223,7 +224,12 @@ Deprecated). the `httpx` exception as `__cause__`. Every `httpx.TransportError` worth sending again (a server that hung up mid-response, a proxy failure, a read or write error) is now retried and raised this way, and a wait polls again - after one; 2.x let these escape as the raw `httpx` exception. A request + after one; 2.x let these escape as the raw `httpx` exception. For those + (`httpx.RemoteProtocolError`, `httpx.ProxyError`, `httpx.ReadError`, + `httpx.WriteError` and the like) the new `LenzConnectionError` is **not** a + subclass of what 2.21 raised: code that caught `httpx.HTTPError` / + `httpx.TransportError` for them no longer catches them; catch + `LenzConnectionError` (or `LenzAPIError`) instead. A request that could never be sent (`httpx.UnsupportedProtocol`, `httpx.LocalProtocolError`) still raises the `httpx` exception. - **A 404 raises `LenzNotFoundError`** (a `LenzError`, as before) and its @@ -406,7 +412,18 @@ rebuild: carried a generic one; a failed poll read back from storage can carry a `hint` 2.x left out. - Some other hints and 4xx messages are worded anew (the `message` / `cause` - of a blank input or an unparseable body, a failed review's hint). + of a blank input other than a blank claim or an unparseable body, a failed + review's hint). +- The values of a `failure` block (`TaskStatus.failure`, `AssessClaim.failure`, + `AssessResponse.failure`) are the server's, where 2.21 rebuilt them from the + 2.x fields: `failure.detail` is the API's sentence ("No sources about the + claim were found.", where 2.21 read "Pipeline stopped at: research_empty", + and on an /assess row a sentence where 2.21 read `None`); on a failed + /assess row `failure.docs_url`, `failure.failure_class` and + `failure.retryable` are filled (2.21: `""`, `""`, `None`), and + `failure.hint` too where the API sends one. `failure.code` reads the same. + The 2.x attributes (`error`, `error_code`, `hint`, ...) keep their 2.x + values. A cancelled task's `failure` is `None` (see Breaking). - Reviews: a review row stored without a failure block (a quick-check row or a `ReviewFailure`) reads one, where 2.x read `failure` `None`; a deep check's `modified_at` is computed from its completion time by the 2.x rule From dea14083cb1e9d558f5e92d7492e546eea826e79 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sat, 10 Oct 2026 00:59:22 +0300 Subject: [PATCH 65/67] fix: a cancelled task status reads the 2.x failure block; clamp the public retry_after - TaskStatus.failure on a cancelled task sent without a block reads the block 2.x read for a run cancelled while running (code and failure_class cancelled, detail "Cancelled.", not retryable), as the Node SDK fills it: get_status, a batch item's status_detail and a verification.cancelled event's verification. Reviews and citation checks keep the server's null. The tests that pinned None are updated. - exc.retry_after: a non-finite or unparseable stated wait is not one (None, or a 429's next stated wait, else 0), and a huge finite one is clamped to 2,147,483 s, the rule the retry ladder applies. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- CHANGELOG.md | 22 ++++--- src/lenz_io/errors.py | 22 +++++-- src/lenz_io/models.py | 20 +++++- tests/test_cancelled.py | 7 ++- tests/test_read_both_shapes.py | 10 +-- tests/test_release_edges.py | 107 +++++++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c8973..915fb63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,11 +59,15 @@ Deprecated). is its own status, `cancelled`**, where 2.x read `failed` with `failure_class` `cancelled`. - `TaskStatus.status`, `ReviewFull.status` and `Citecheck.status` can read - `"cancelled"`, and `.failure` is `None` on those bodies. A hand-written - polling loop must treat `cancelled` as terminal or it polls until its own - timeout. A `TaskStatus` still carries the 2.x fields (`error` + `"cancelled"`. A hand-written polling loop must treat `cancelled` as + terminal or it polls until its own timeout. A cancelled `TaskStatus` + (from `get_status`, a batch item's `status_detail`, or a + `verification.cancelled` event's `verification`) reads the failure block + 2.x read for it (`failure.code` and `failure_class` `"cancelled"`, + `detail` `"Cancelled."`, `retryable` `False`) and the 2.x fields (`error` `"Cancelled."`, `failure_class`, `failure_reason`, `retryable`, - `docs_url`). + `docs_url`). A cancelled review or citation check has `failure` `None`, as + the API sends it. - The wait helpers (`wait`, `verify_and_wait`, `verify_batch_and_wait`, `review_and_wait`, `citecheck_and_wait`) end on it at once instead of polling to their timeout. They raise the same error class and failure @@ -100,9 +104,11 @@ Deprecated). `TaskAccepted.chain_id` attribute is kept and reads `""` (deprecated). - **A `Retry-After` the SDK cannot use is read safely.** A non-finite value (`inf`, `-inf`, `nan`, `1e999`) reads as no stated wait, so the normal - backoff runs, as in the Node SDK, where it raised `OverflowError`; on the - error's `retry_after` it reads `None` (unknown). A huge finite wait is - clamped to 2,147,483 seconds, still past every cap. + backoff runs, as in the Node SDK, where it raised `OverflowError`. A huge + finite wait is clamped to 2,147,483 seconds, still past every cap. The + error's `retry_after` follows the same rule: a non-finite or unparseable + value is not a stated wait (`None`; on a `LenzRateLimitError` the next + stated wait, else `0`, as before), and a larger one reads 2,147,483. - **A cancel's error body is read as sent**: its `code` (a 422's `validation_error` too), `detail` and `errors`. - **One rule for every timeout and retry count of a request, checked before @@ -423,7 +429,7 @@ rebuild: `failure.retryable` are filled (2.21: `""`, `""`, `None`), and `failure.hint` too where the API sends one. `failure.code` reads the same. The 2.x attributes (`error`, `error_code`, `hint`, ...) keep their 2.x - values. A cancelled task's `failure` is `None` (see Breaking). + values. - Reviews: a review row stored without a failure block (a quick-check row or a `ReviewFailure`) reads one, where 2.x read `failure` `None`; a deep check's `modified_at` is computed from its completion time by the 2.x rule diff --git a/src/lenz_io/errors.py b/src/lenz_io/errors.py index c9cb0e8..c241b25 100644 --- a/src/lenz_io/errors.py +++ b/src/lenz_io/errors.py @@ -933,17 +933,17 @@ def _read(key: str, block_key: str) -> Any: err.fix = "Its account's retention period removed it. A certificate issued for it is still available." if isinstance(err, LenzAPIError) and not isinstance(err, LenzUpstreamUnavailableError): - err.retry_after = _opt_int(headers.get("Retry-After") or headers.get("retry-after")) + err.retry_after = _opt_wait(headers.get("Retry-After") or headers.get("retry-after")) if isinstance(err, LenzUpstreamUnavailableError): # Body ``retry_after`` first (both 503 shapes carry it), header as # the fallback for any proxy that strips the body. - stated = _opt_int(parsed.get("retry_after")) + stated = _opt_wait(parsed.get("retry_after")) if stated is None: # The /review error body states its wait under this name. - stated = _opt_int(parsed.get("retry_after_seconds")) + stated = _opt_wait(parsed.get("retry_after_seconds")) if stated is None: - stated = _opt_int(headers.get("Retry-After") or headers.get("retry-after")) + stated = _opt_wait(headers.get("Retry-After") or headers.get("retry-after")) err.retry_after = stated if isinstance(err, LenzQuotaExceededError): @@ -1014,7 +1014,7 @@ def _read(key: str, block_key: str) -> Any: parsed.get("retry_after_seconds"), parsed.get("retry_after"), ): - resolved = _opt_int(candidate) + resolved = _opt_wait(candidate) if resolved is not None: err.retry_after = resolved break @@ -1036,6 +1036,18 @@ def _read(key: str, block_key: str) -> Any: return err +#: The longest wait ``retry_after`` reports, in seconds (the client's +#: ``MAX_TIMEOUT_SECONDS``): a larger stated wait reads as this. +_MAX_STATED_WAIT = 2_147_483 + + +def _opt_wait(value: Any) -> int | None: + """A stated wait in seconds, as ``_opt_int`` reads it (a non-finite or + unparseable value is ``None``), at most ``_MAX_STATED_WAIT``.""" + seconds = _opt_int(value) + return None if seconds is None else min(seconds, _MAX_STATED_WAIT) + + def _opt_int(value: Any) -> int | None: """Coerce to int, or None when absent/unparseable. diff --git a/src/lenz_io/models.py b/src/lenz_io/models.py index 5a87310..4364dcc 100644 --- a/src/lenz_io/models.py +++ b/src/lenz_io/models.py @@ -982,6 +982,18 @@ def _original_status_error(code: Any, detail: Any) -> Any: return detail +#: The failure block of a task cancelled elsewhere, which the API sends with +#: no block: what 2.x read for a run cancelled while running. +_CANCELLED_TASK_FAILURE: dict[str, Any] = { + "code": "cancelled", + "detail": "Cancelled.", + "hint": None, + "failure_class": "cancelled", + "retryable": False, + "docs_url": "https://lenz.io/docs/errors#cancelled", +} + + class TaskStatus(_Lax): """Returned by ``GET /verify/status/{task_id}``.""" @@ -1070,8 +1082,14 @@ def failure(self) -> FailureBlock | None: ``docs_url``. ``None`` on other statuses. Replaces ``error``, ``failure_reason``, ``failure_detail``, ``failure_class``, ``retryable``, ``docs_url`` and ``hint`` on a failure, which are - deprecated and kept.""" + deprecated and kept. + + On ``cancelled`` with no block sent: the block 2.x read for a run + cancelled while running (``code`` and ``failure_class`` + ``cancelled``, ``detail`` "Cancelled.", not retryable).""" sent = _sent(self, "failure") + if not isinstance(sent, dict) and self.status == "cancelled": + sent = _CANCELLED_TASK_FAILURE if not isinstance(sent, dict): return None # A verification spelled "nothing checkable" ``not_a_claim``. diff --git a/tests/test_cancelled.py b/tests/test_cancelled.py index b898cc3..f94ac7d 100644 --- a/tests/test_cancelled.py +++ b/tests/test_cancelled.py @@ -61,7 +61,9 @@ def test_get_status_returns_it_and_does_not_raise(self, client: Lenz, name: str) assert isinstance(status, TaskStatus) assert status.status == "cancelled" assert status.task_id == body["task_id"] - assert status.result is None and status.failure is None + assert status.result is None + # The block 2.x read for a run cancelled while running, as the Node SDK fills it. + assert status.failure is not None and status.failure.code == "cancelled" def test_wait_raises_the_failed_error_at_once(self, client: Lenz, slept: list[float], name: str) -> None: body = _body(name) @@ -427,7 +429,8 @@ def test_a_cancelled_status_with_a_null_failure_still_reads_the_2x_fields() -> N "cancelled", False, ) - assert status.failure is None + assert status.failure is not None + assert (status.failure.code, status.failure.detail, status.failure.retryable) == ("cancelled", "Cancelled.", False) def test_the_original_shape_still_reads_as_a_failure(client: Lenz) -> None: diff --git a/tests/test_read_both_shapes.py b/tests/test_read_both_shapes.py index ac8b4df..a936193 100644 --- a/tests/test_read_both_shapes.py +++ b/tests/test_read_both_shapes.py @@ -159,14 +159,16 @@ def test_failed_status_failure_block(name): @pytest.mark.parametrize("name", ["verify__status_cancelled_live.json", "verify__status_cancelled_durable.json"]) -def test_cancelled_status_has_no_failure_block(name): - """A task cancelled elsewhere is its own status with no failure block; the - 2.x fields still read what the original shape said of it.""" +def test_cancelled_status_reads_the_2x_failure_block(name): + """A task cancelled elsewhere is its own status, sent with no failure + block; ``failure`` reads the block 2.x read for it (as in Node), and the + 2.x fields what the original shape said of it.""" legacy_body, body = _both(name) assert "failure" not in body canonical = TaskStatus.model_validate(body) assert canonical.status == "cancelled" - assert canonical.failure is None + assert canonical.failure is not None + assert (canonical.failure.code, canonical.failure.failure_class) == ("cancelled", "cancelled") assert canonical.failure_reason == legacy_body["failure_reason"] == "cancelled" assert canonical.failure_class == legacy_body["failure_class"] == "cancelled" assert canonical.retryable is False diff --git a/tests/test_release_edges.py b/tests/test_release_edges.py index f6285a7..bcf29d3 100644 --- a/tests/test_release_edges.py +++ b/tests/test_release_edges.py @@ -136,3 +136,110 @@ def test_a_cancel_schema_422_keeps_its_code_and_errors(path: str) -> None: err = map_response_to_error(422, json.dumps(body).encode(), {}, endpoint=("POST", path)) assert err.code == "validation_error" assert err.message == "Field required" + + +# ── a cancelled task status carries the failure block 2.21 built ─────────── + +_CANCELLED_BLOCK = { + "code": "cancelled", + "detail": "Cancelled.", + "hint": None, + "failure_class": "cancelled", + "retryable": False, + "docs_url": "https://lenz.io/docs/errors#cancelled", + "failure_reason": "cancelled", +} + + +def _block(failure: Any) -> dict[str, Any]: + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return {k: getattr(failure, k) for k in _CANCELLED_BLOCK} + + +def test_a_cancelled_task_status_has_the_cancelled_failure_block() -> None: + from lenz_io import TaskStatus + + status = TaskStatus.model_validate({"status": "cancelled", "task_id": "t1"}) + assert status.failure is not None + assert _block(status.failure) == _CANCELLED_BLOCK + + +def test_a_sent_failure_block_on_a_cancelled_status_is_read_as_sent() -> None: + from lenz_io import TaskStatus + + status = TaskStatus.model_validate( + {"status": "cancelled", "task_id": "t1", "failure": {"code": "other", "detail": "x."}} + ) + assert status.failure is not None and status.failure.code == "other" + + +def test_other_statuses_still_have_none() -> None: + from lenz_io import TaskStatus + + assert TaskStatus.model_validate({"status": "processing", "task_id": "t1"}).failure is None + assert TaskStatus.model_validate({"status": "completed", "task_id": "t1"}).failure is None + + +def test_the_cancelled_webhooks_verification_carries_it_too() -> None: + from lenz_io import VerificationCancelled, parse_webhook + + event = parse_webhook( + { + "event": "verification.cancelled", + "event_id": "e1", + "task_id": "t1", + "status": "cancelled", + "verification": {"status": "cancelled", "task_id": "t1"}, + } + ) + assert isinstance(event, VerificationCancelled) + assert event.verification is not None and _block(event.verification.failure) == _CANCELLED_BLOCK + + +def test_a_batch_items_cancelled_status_detail_carries_it(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("lenz_io.client.time.sleep", lambda s: None) + with respx.mock(base_url=DEFAULT_BASE_URL) as r: + r.post("/verify/batch").respond(200, json={"batch_id": "b", "items": [{"task_id": "t1", "claim": "A."}]}) + r.get("/verify/status/t1").respond(200, json={"status": "cancelled", "task_id": "t1"}) + [item] = Lenz(api_key=KEY).verify_batch_and_wait(claims=[{"claim": "A."}]) + assert item.status == "failed" and item.status_detail is not None + assert _block(item.status_detail.failure) == _CANCELLED_BLOCK + + +def test_reviews_and_citation_checks_keep_the_servers_null() -> None: + from lenz_io import Citecheck, ReviewFull + + review = ReviewFull.model_validate( + {"review_id": "r1", "status": "cancelled", "issues": [], "failures": [], "claims": []} + ) + check = Citecheck.model_validate( + {"citecheck_id": "c1", "status": "cancelled", "citations": [], "citation_issues": [], "citation_failures": []} + ) + assert review.failure is None and check.failure is None + + +# ── the public retry_after follows the same rule as the retry ladder ─────── + + +@pytest.mark.parametrize( + ("status", "body", "header", "expected"), + [ + (503, {"detail": "busy"}, "1e300", 2_147_483), + (503, {"detail": "busy"}, "inf", None), + (503, {"detail": "busy"}, "nan", None), + (503, {"detail": "busy"}, "30", 30), + (503, {"code": "capacity", "detail": "busy", "retry_after": 1e300}, None, 2_147_483), + (503, {"code": "capacity", "detail": "busy", "retry_after": "inf"}, "45", 45), + (429, {"code": "rate_limited", "detail": "slow"}, "1e300", 2_147_483), + (429, {"code": "rate_limited", "detail": "slow", "reset_in_seconds": 7}, "inf", 7), + # A 429's retry_after is a number, 0 when nothing usable is stated (as in Node). + (429, {"code": "rate_limited", "detail": "slow"}, "inf", 0), + ], +) +def test_the_public_retry_after(status: int, body: dict[str, Any], header: str | None, expected: Any) -> None: + headers = {"Retry-After": header} if header is not None else {} + err = map_response_to_error(status, json.dumps(body).encode(), headers) + assert err.retry_after == expected From bf322e7a6edd6c5291e48a55da0e3638e76a01a6 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sat, 10 Oct 2026 01:21:46 +0300 Subject: [PATCH 66/67] chore: refresh openapi.json (public reference text, 3.0 code samples) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- openapi.json | 58 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/openapi.json b/openapi.json index 0d48ff3..bb5bb44 100644 --- a/openapi.json +++ b/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Lenz \u2014 AI Fact-Checking API", "version": "1.0.0", - "description": "# Four API calls and one to run them\n\nA research-depth ladder: find claims, judge fast, prove deep, citecheck; `/review` runs them all.\n\n- `POST /extract` \u2014 enumerate every major verifiable claim in a document (up to 100, most check-worthy first, inputs to 50k chars), or in a public web page given its URL; an optional `focus` narrows the list to the claims you care about, and `locate: true` keeps only the claims traced back to the text, with where each is made. Costs no credits; fair-use cap 1,000/account/day.\n- `POST /assess` \u2014 fast 3-model frontier panel verdict, typically 10-25s. Sync, 1 credit per claim. Send one text in `claim`, or a list of up to 20 in `claims` \u2014 one row per claim, same order, all checked in parallel. A text that makes more claims than one call checks gets the most check-worthy checked and the rest back as `more_claims`, free, to send on.\n- `POST /verify` \u2014 full multi-model pipeline with citations in ~90s. Async, 10 credits \u2014 or 5 with `depth: \"low\"`, a shallower check that comes back sooner.\n- `POST /citecheck` \u2014 does each cited source say what the text attributes to it: send pairs of a statement and its URL or DOI, or a whole text. Async; 1 credit per checked citation, refunded when a citation could not be checked ([guide](https://lenz.io/docs/citations)).\n- `POST /review` \u2014 the extract \u2192 assess \u2192 verify ladder on a whole draft, returning the issues with suggested rewrites in 2-4 min. Async, 1 credit per claim assessed + 10 (5 at `depth: \"low\"`) per claim verified. With `escalate.max_citations` (0-20, default 0) it also checks whether each source the draft cites says what the draft attributes to it, at 1 credit per checked citation (one that could not be checked is refunded) ([guide](https://lenz.io/docs/citations)). `more_claims` and `more_citations` list what the review found and did not check, at no cost; send them to `/assess` and `/citecheck` to check the rest.\n- Also `POST /ask/{id}` \u2014 ask follow-up questions grounded on a verification.\n\n## Built for teams whose AI output is async or document-shaped\n\nLegal-memo generators, AI deep-research, due-diligence platforms, vertical agents producing structured deliverables, \"report-as-a-service\" companies. `/assess` opens the door to sync UX too \u2014 fast enough to gate a chat completion or a UI submit.\n\n## API versions\n\nLenz versions the shape of its responses by date: send `X-Lenz-API-Version: 2026-10-11` to choose this shape. This reference describes `2026-10-11`. A field `2026-05-13` still sends under an older name is listed `deprecated` beside the current one, and where its shape differs, the operation names the `...Legacy` component it receives (`x-lenz-versions` on the response). Every response carries `X-Lenz-API-Version`, naming the version it is in. Which version a request gets without the header, and what changed: [https://lenz.io/docs/api-versions](https://lenz.io/docs/api-versions).\n\n## Install the SDK\n\n```bash\npip install lenz-io # Python\nnpm install lenz-io # Node / TypeScript\n```\n\nSource on GitHub: [lenz-io-python](https://github.com/lenzhq/lenz-io-python) \u00b7 [lenz-io-node](https://github.com/lenzhq/lenz-io-node).\n\n## The canonical integration: one /review call\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# Extract, assess, and verify the doubtful claims in one call (async, 2-4 min)\ndraft = \"The EU AI Act took effect in March 2024. It sorts AI systems into four risk tiers.\"\nreview = client.review_and_wait(text=draft)\nfor i in review.issues:\n print(i.verdict, i.claim, i.suggested_rewrite)\n```\n\nThe default policy: a quick verdict on every claim (up to 20), and the deep check on the rows whose quick verdict is `False`, `Mostly False` or `Mixed`, or whose confidence is `low`, up to five. Change it in `escalate`. To deep-check the rows the cap left out (`escalation.disposition == \"cap\"`), send them to `/verify/batch`. A resend with the same `Idempotency-Key` within 24 hours returns the same review; a new key is a new review.\n\n## The primitives, call by call: extract \u2192 assess \u2192 conditional verify\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# 1. Enumerate the factual claims in your model output (free)\n# extracted.claims lists every claim found, most check-worthy first:\n# always a list (one entry for one claim, [] for none).\n# Pass focus=\"market size and competitors\" to narrow the list;\n# status is \"no_match\" when the document has claims but none match.\nextracted = client.extract(text=llm_output)\nclaims = [c.claim for c in extracted.claims]\n\n# 2. Fast verdict in ONE call (typically 10-25s, 3-model panel): a list of up\n# to 20, one row per claim, in the order you sent them. extract finds up to\n# 100, most check-worthy first, so this checks the first 20.\nquick = client.assess(claims=claims[:20]).claims if claims else []\n\n# 3. Escalate the low-confidence ones to /verify in one batch (~90s, multi-model pipeline)\n# Rows we ran out of time on are free and worth resending; the rest of the\n# failed rows (no_checkable_claim, framing_failed) want a different input.\nretry = [c.claim for c in quick if c.failure and c.failure.code == \"timeout\"]\nquick += client.assess(claims=retry).claims if retry else []\ndoubtful = [{\"claim\": c.claim} for c in quick if c.status == \"completed\" and c.confidence == \"low\"]\nfor r in client.verify_batch_and_wait(claims=doubtful) if doubtful else []:\n if r.verification:\n print(r.verification.verdict, r.verification.lenz_score, r.verification.key_finding)\n```\n\n## How /verify works under the hood\n\nFrame \u2192 Collect Evidence \u2192 Debate (2 models, 2 rounds) \u2192 Adjudicate (three reviewers on models from different vendors, two more when they disagree) \u2192 Conclude. ~90 seconds wall-clock per claim.\n\n### While /verify is running\n\n`GET /verify/status/{task_id}` answers one of five shapes, keyed on `status`: `processing`, `needs_input`, `completed`, `failed`, and, after you cancel the run (see Stopping a run), `cancelled`. Every one echoes `task_id`, and only the fields belonging to that shape are present \u2014 read the ones for the branch you are in, and treat an absent key as absent rather than empty.\n\nA `processing` body carries `progress`:\n\n```json\n{\"status\": \"processing\", \"task_id\": \"\u2026\",\n \"progress\": {\"step\": \"research\", \"index\": 2, \"total\": 5,\n \"elapsed_seconds\": 42, \"poll_after_seconds\": 5}}\n```\n\n`step` is a closed set \u2014 `starting`, `framing`, `research`, `debate`, `adjudication`, `conclusion` \u2014 so it is safe to switch on. `index` is the 1-based stage position (0 while `starting`) out of `total`; read `total` off the response rather than hard-coding it. `poll_after_seconds` is how long to wait before looking again, and `elapsed_seconds` is how long the run has been going \u2014 a measurement, not an estimate of what is left.\n\nNote that `index` is stage **position**, not elapsed work: the stages are uneven, so a progress bar driven by it will sit on `research` for roughly half the run.\n\n`progress` is **advisory**. It says the work is pending and which stage it is on; it is not a place to read results from. The verdict arrives once, under `result`, when `status` is `completed`.\n\n### When /verify asks for input\n\n`/verify` is not only submit-and-poll. `GET /verify/status/{task_id}` can terminate at `needs_input` \u2014 the submitted text held several claims, or was too vague to check as written \u2014 and the task waits rather than guessing which claim you meant. Several claims in one sentence count the same as several sentences.\n\nResolve it with `POST /verify/{task_id}/select`, passing the exact wording of the claim(s) you want from the ones the status offered. Selection is **by text, not index**; anything that was not offered is rejected with a 422. Each selected claim fans out into its own pipeline, so the response carries one `task_id` per claim \u2014 poll each the way you would a fresh `/verify`.\n\nClaims returned by `/extract` skip this entirely: submitted verbatim, they are never bounced back for rephrasing.\n\n### Covered verification\n\nEvery verification a paid Pro or Scale account runs is put in front of a gate. The ones that qualify carry a contractual warranty, and every result tells you whether it qualified \u2014 a `coverage` block on the completed `/verify/status/{task_id}` body, on `GET /verifications/{id}` and on the `verification.completed` webhook.\n\nIt is on by default, with no request field to set; an account on Pro or Scale can turn certificates off on the API credentials page, for checks submitted from then on (reason `account`; a verification that already carries a certificate keeps it).\n\n`status` is `covered`, `pending_timestamp` or `uncovered`; `reasons` says why not, in a closed seven-value vocabulary. **If you publish automatically, key on the `certificate.timestamped` webhook, not `verification.completed`** \u2014 cover requires the qualified timestamp to precede what you publish, and `completed` fires while the qualified timestamp is still pending.\n\nFull wire format, the reason vocabulary and the offline verifier: [lenz.io/docs/coverage](https://lenz.io/docs/coverage). What is covered and what is not: [lenz.io/warranty](https://lenz.io/warranty).\n\n### When /verify fails\n\nA `failed` status (and the `verification.failed` webhook) carries two axes. `failure_reason` says **where** the pipeline stopped (`research_empty`, `conclusion_failed`, `task_stuck`, \u2026); on the webhook the same value rides in `error`. `failure_class` says **why**, and `retryable` is derived from it:\n\n| `failure_class` | Meaning | `retryable` |\n|---|---|---|\n| `upstream_unavailable` | Model or search providers were rate-limited or down, or the task was never run \u2014 resubmit after a short wait | `true` |\n| `insufficient_evidence` | We looked; the sources are not there | `false` |\n| `invalid_input` | Not a checkable claim, an unreadable URL | `false` |\n| `cancelled` | You stopped it with `POST /verify/{task_id}/cancel`; not charged | `false` |\n| `internal` | Anything else \u2014 retrying the same input will not help | `false` |\n\nBranch on `retryable`, not on `failure_reason`: the same reason can mean either. `failure_class` is a **closed set** \u2014 these five values are stable identifiers. `failure_reason` is an open, informational set (`task_error`, `task_stuck`, `not_a_claim`, \u2026 will appear). A failed body also carries `docs_url`, pointing at the explanation of its class. A failed `/verify` is not charged.\n\nIn the next API version a cancelled run is not a failure: `GET /verify/status/{task_id}` answers `{\"status\": \"cancelled\", \"task_id\": \"\u2026\"}`, and a `webhook_url` receives `verification.cancelled`. A request made in 2026-05-13 keeps `status: \"failed\"` with `failure_class: \"cancelled\"` and receives `verification.failed`.\n\n### Stopping a run\n\n`POST /verify/{task_id}/cancel` stops a verification that has not finished. A cancelled run is not charged and saves no verification; model work already under way is not billed to you, and the run stops at its next step. The call is safe to repeat and answers 200 in every state: `cancelled: true` with `status: \"cancelled\"`, or `cancelled: false` with `status: \"completed\"` (it finished first and was charged as usual) or `\"failed\"`. It needs no `Idempotency-Key`. An unknown task, another account's task and a task not submitted through the API answer 404 `not_found`; a task that is a `/review`'s deep check answers 409 `use_review_cancel`. Each item of a `/verify/batch` has its own `task_id`, so cancel per task; there is no call for a whole batch. `POST /reviews/{review_id}/cancel` and `POST /citechecks/{citecheck_id}/cancel` stop everything in a review or a citation check. They refund the quick checks not yet served and the citations not yet checked, and keep what was delivered charged.\n\nOne of those reasons comes from framing rather than from a pipeline step and carries `failure_class: invalid_input`: `not_a_claim` (the input is not a checkable statement, or is too vague to have a checkable reading). A vague input that has a likely reading is verified on it, and the result's `claim` is that reading. On the webhook the same value rides in `error`, as an ordinary `verification.failed` delivery; before 2026-09-03 it fired nothing at all, so a task submitted with a `webhook_url` could terminate in silence.\n\n### What may change without notice\n\nNew keys may be added to status and webhook bodies at any time \u2014 ignore keys you do not recognise. Fields documented as **advisory** \u2014 `progress` and everything inside it \u2014 may also change or disappear without a version bump. The verdict fields under `result` may not: those change only with a versioned, announced release.\n\n## Common patterns\n\n- **Runtime sync UX**: `POST /assess` on the user-facing path; escalate `confidence == \"low\"` claims to `/verify` in the background.\n- **Runtime webhook**: `POST /verify` with `webhook_url`. Lenz POSTs the typed payload when done. Best for document pipelines.\n- **Whole drafts**: `POST /review` with `webhook_url`; handle `review.completed` (a `ReviewWebhookPayload`, deduped on `event_id`) and read `review.issues[]` and `review.citation_issues[]`.\n- **CI / pre-release**: `/extract` + batch `/verify` your golden set on every deploy. The free `/extract` tier covers most of this. Claims returned by `/extract` verify cleanly \u2014 submitted verbatim to `/verify`, they are never bounced back for rephrasing.\n- **Incident triage**: paste the offending output into `/extract`, then `/verify` the wrong-looking claims. Get a citation trail to send back to the customer.\n\n## What's in the response\n\nUnified vocabulary across every claim-shaped response:\n\n- **`claim`** \u2014 the framed claim text.\n- **`verdict`** \u2014 `\"True\" | \"Mostly True\" | \"Mixed\" | \"Mostly False\" | \"False\" | \"Error\"`.\n- **`confidence`** \u2014 `\"high\" | \"medium\" | \"low\"`.\n- **`lenz_score`** \u2014 1\u201310 integer score (deep payloads).\n- **`key_finding`** \u2014 one declarative sentence stating the most important fact the analysis established. For a false claim it states the CORRECTED fact rather than restating the claim, so render it next to the verdict label, never as a standalone headline. `\"\"` on older verifications predating the field.\n- **`suggested_rewrite`** \u2014 a suggested rewrite of `claim` that the verification's findings support. It has not been verified itself: before using it, review it or run it through `/verify`. `null` for a true claim, when no correction is established, and on older verifications. On every verification, single or listed (`GET /verifications/{id}`, `/verify/status`, the `verification.completed` webhook, `GET /verifications`, `GET /library`). On `/assess` rows too when the request sets `suggest_rewrite`: written from the quick check's reasoning for a claim it found False or Mostly False with high confidence, at no extra credit; `null` otherwise, and on every row when not requested.\n- **`sources`** \u2014 evidence with title, URL and snippet: the sentence(s) quoted from the page, verbatim and in the page's own language (`/verify` only).\n- **`audit`** \u2014 adjudication reasoning, debate transcript, panel agreement (`/verify` only).\n\n## How the API is organized\n\n- **Extract** \u2014 pull verifiable claims out of text.\n- **Assess** \u2014 fast 3-model verdict.\n- **Verify** \u2014 deep multi-model pipeline: submit one claim or a batch, poll status, and resolve a `needs_input` interrupt by selecting claims.\n- **Verifications** \u2014 list, fetch, delete, and toggle visibility on stored verifications.\n- **Library** \u2014 browse the public catalog. No API key needed.\n- **Ask** \u2014 follow-up questions, 1 credit each.\n- **Account** \u2014 your credit balance and the price list.\n\n## Credits\n\nOne pool per account funds every billable call, at a fixed weight:\n\n| Endpoint | Credits |\n|---|---|\n| `POST /verify` (and `/verify/batch`, `/select`) | 10 per claim |\n| `POST /verify` with `depth: \"low\"` | 5 per claim |\n| `POST /assess` | 1 per claim \u2014 per row that carries a verdict; error rows and rows answered from the cache are free |\n| `POST /ask` | 1 |\n| `POST /extract` | 0 \u2014 free, fair-use capped |\n\n`depth: \"low\"` caps research breadth \u2014 fewer discovery queries, a hard extraction ceiling, no recovery tiers on the happy path \u2014 while every reasoning step runs unchanged. It is not a model downgrade. You are charged for the depth you REQUESTED. A `/verify/batch` may mix depths per item and is billed per item.\n\nAn answer served from the last hour's cache is free: a `/verify`, `/assess` or `/review` claim that gets back a verdict checked in the last hour costs nothing, so a tool that resends a request is not charged twice. The exception is a `/verify` that issues you a new warranty certificate, charged at the depth you requested.\n\n`GET /me/usage` reports the balance under `credits` and the weights under `costs`, with parameter-dependent prices such as `depth` nested under `cost_options`. In version `2026-05-13` the per-capability blocks beside them (`verify`, `assess`, `ask`) are **projections** of that one balance into each capability's own unit \u2014 how many of those calls the remaining credits would buy \u2014 not separate allowances. Spending on any one of them moves all of them.\n\n## Authentication\n\n1. [Create a Lenz account](https://lenz.io/auth).\n2. Generate an API key on the [API credentials](https://lenz.io/api-credentials) page.\n3. Send it on every request:\n ```\n Authorization: Bearer lenz_...\n ```\n\nOr skip steps 1\u20132: **[lenz.io/setup](https://lenz.io/setup)** issues a test key and generates setup instructions for your environment \u2014 MCP client, SDK or plain HTTP \u2014 that you paste into your coding agent to do the wiring.\n\n### One balance, whichever door you come through\n\nLenz also runs a remote MCP server at `https://lenz.io/mcp`, so an agent can fact-check inside a conversation. It is **not** part of this surface \u2014 there are no MCP endpoints below, and its tools are documented at [lenz.io/integrations/mcp-server](https://lenz.io/integrations/mcp-server).\n\nIt matters here for one reason: that server is a client of this API. It takes the same `lenz_...` key and forwards it to the endpoints below, so MCP traffic spends the same credits, counts against the same rate limits, and shows up in `GET /me/usage` \u2014 there is no separate MCP quota to check, and a 402 can just as easily be an agent as your own code.\n\n## Idempotency\n\nSend an `Idempotency-Key` header on `POST /extract`, `POST /assess`, `POST /verify`, `POST /verify/batch`, `POST /verify/{task_id}/select`, `POST /review`, `POST /citecheck` and `POST /ask/{verification_id}` so retries after a network drop do not spawn duplicate tasks or double-debit quota. The server caches the response body and returns it on replay; a re-used key with a different body returns 422. The SDKs generate a key for `extract`, `assess`, `select`, `verify`, `verify_and_wait`, `review` and `citecheck` and reuse it across their own retries; `verify_batch` and `ask.send` send one only when you pass `idempotency_key` yourself.\n\nOn `/ask` a key also keeps the retry out of the conversation: without one, a repeated question is a second turn, a second credit and a second answer that the next turn reads as context. A retry that lands while the first call is still running gets 409 rather than the reply \u2014 nothing is finalized yet to replay \u2014 so treat it as work already in progress and retry the same key once the first call has had time to finish. `GET /ask/{verification_id}` returns the message history, but carries no request id, so it can only tell you which answer was yours when one question is in flight at a time. The SDKs send the header when you pass a key yourself: `ask.send(..., idempotency_key=...)` in Python, `ask.send(id, { message, idempotencyKey })` in Node; neither generates one for ask, because a repeated question is usually deliberate. The n8n node's Ask operation sends a key scoped to the workflow execution, so a retry replays rather than re-asks. The Zapier app sends none on ask.\n\n## Errors\n\nThe status code says what to do next; the body says what happened. Every response carries an `X-Request-ID` header \u2014 quote it on a support ticket.\n\n| Status | Condition | Retry? |\n|---|---|---|\n| 401 | Missing, malformed or unknown key | After fixing credentials |\n| 402 | Out of credits, or the plan is the limit | No \u2014 billing action |\n| 403 | Authenticated but not allowed (private verification, IP block) | No |\n| 409 | A request with the same `Idempotency-Key` is still running, or the work the call names is not in the state it needs (`no_selection_pending`, `verification_not_ready`, `verification_failed`, `use_review_cancel`) | Same-key conflict: yes, with the same key. `verification_not_ready`: poll, or choose the claims when it needs input. `no_selection_pending`: poll the status first. `use_review_cancel`: cancel the review instead |\n| 422 | Malformed input | No |\n| 429 | Rate limit: the per-account `/extract` daily cap, or too many reviews or citation checks running (`review_in_flight`, `citecheck_in_flight`) | Yes, after `Retry-After` |\n| 502 | A sync step (`/assess`, `/extract`, `/ask`) failed deterministically \u2014 a schema or parse failure (`/ask`: `ask_failed`) | No \u2014 same input, same outcome |\n| 503 | Our model providers were rate-limited or down for the step and every fallback was exhausted (`code: upstream_unavailable`) or the pipeline has no capacity (`code: capacity`) | Yes, after `Retry-After` (body `retry_after` too) |\n\n**402** carries `detail`, `code` (always `no_credits`), `upgrade_url`, `doc_url`, `wall_id`, `cost` (what the rejected call would have taken), and \u2014 when resolvable \u2014 `remaining`, `credits_remaining` and `resets_at`. `remaining` and `requested` are in the *capability's* unit (verifications, assesses); `credits_remaining` and `cost` are in credits, so a client can tell \"not enough for a 10-credit verify\" from \"empty\". The resolvable fields are omitted rather than sent as `null`, so an absent key means \"unknown\", not \"zero\".\n\n`wall_id` identifies this specific rejection and is already appended to `upgrade_url`, so sending a user to that URL is all that is needed \u2014 it exists as its own field only for clients that build their own upgrade link. Treat `upgrade_url` as opaque and follow it whole rather than matching it against a fixed string.\n\nThe `code` values are deliberately plan-agnostic: they name the condition, never the plan that would fix it. Send users to `upgrade_url` rather than mapping codes to plan names \u2014 the set of plans changes, the conditions do not.\n\nAn empty balance is deliberately **not** a 429: a 429 tells every client to retry, and an empty balance never succeeds on retry. **429** carries `code`, `limit`, `reset_in_seconds`, `upgrade_url` and the `Retry-After` header. The in-flight 429 of `/review` and `/citecheck` (`review_in_flight`, `citecheck_in_flight`) carries `retry_after_seconds` and `Retry-After` instead. The daily cap is the same on every plan; only a key Lenz has marked unlimited for `/extract` bypasses it.\n\n**Migrating (August 2026).** These rejections were 403, which the SDKs map to their auth error; they are now 402, which maps to the quota error \u2014 not a subclass. If you catch the auth error to handle an empty balance, that branch stops firing. This reaches you on every SDK version, including ones released before the change. `detail` strings are unchanged. Full reference: [lenz.io/docs/errors](https://lenz.io/docs/errors#quota).\n\n## Webhooks\n\nOn `POST /verify` and `POST /review` you may supply a `webhook_url`. We POST an HMAC-SHA256-signed JSON payload to it when the pipeline terminates. Signature header is `X-Lenz-Signature: sha256=`. Verify with your webhook secret (generate + rotate on [API credentials](https://lenz.io/api-credentials)). Retries: 10s, 60s, 600s (3 retries after the initial delivery, 4 attempts total). The SDKs ship a `LenzWebhooks` helper that verifies signatures and parses the payload into typed events. Events: `verification.completed`, `verification.failed`, `verification.cancelled`, `certificate.timestamped`, and for a review `review.completed` / `review.failed` / `review.cancelled` and for a citation check `citecheck.completed` / `citecheck.failed` / `citecheck.cancelled` (switch on `event`; a review's own deep checks fire no `verification.*` events). The `*.cancelled` events fire once, when you cancel, in the next API version; a submission made in 2026-05-13 receives the matching `*.failed` event with `failure_class: \"cancelled\"` instead.\n\nFor commercial use, volume pricing, or onboarding support, [get in touch](https://lenz.io/contact).", + "description": "# Four API calls and one to run them\n\nA research-depth ladder: find claims, judge fast, prove deep, citecheck; `/review` runs them all.\n\n- `POST /extract` \u2014 enumerate every major verifiable claim in a document (up to 100, most check-worthy first, inputs to 50k chars), or in a public web page given its URL; an optional `focus` narrows the list to the claims you care about, and `locate: true` keeps only the claims traced back to the text, with where each is made. Costs no credits; fair-use cap 1,000/account/day.\n- `POST /assess` \u2014 fast 3-model frontier panel verdict, typically 10-25s. Sync, 1 credit per claim. Send one text in `claim`, or a list of up to 20 in `claims` \u2014 one row per claim, same order, all checked in parallel. A text that makes more claims than one call checks gets the most check-worthy checked and the rest back as `more_claims`, free, to send on.\n- `POST /verify` \u2014 full multi-model pipeline with citations in ~90s. Async, 10 credits \u2014 or 5 with `depth: \"low\"`, a shallower check that comes back sooner.\n- `POST /citecheck` \u2014 does each cited source say what the text attributes to it: send pairs of a statement and its URL or DOI, or a whole text. Async; 1 credit per checked citation, refunded when a citation could not be checked ([guide](https://lenz.io/docs/citations)).\n- `POST /review` \u2014 the extract \u2192 assess \u2192 verify ladder on a whole draft, returning the issues with suggested rewrites in 2-4 min. Async, 1 credit per claim assessed + 10 (5 at `depth: \"low\"`) per claim verified. With `escalate.max_citations` (0-20, default 0) it also checks whether each source the draft cites says what the draft attributes to it, at 1 credit per checked citation (one that could not be checked is refunded) ([guide](https://lenz.io/docs/citations)). `more_claims` and `more_citations` list what the review found and did not check, at no cost; send them to `/assess` and `/citecheck` to check the rest.\n- Also `POST /ask/{id}` \u2014 ask follow-up questions grounded on a verification.\n\n## Built for teams whose AI output is async or document-shaped\n\nLegal-memo generators, AI deep-research, due-diligence platforms, vertical agents producing structured deliverables, \"report-as-a-service\" companies. `/assess` opens the door to sync UX too \u2014 fast enough to gate a chat completion or a UI submit.\n\n## API versions\n\nLenz versions the shape of its responses by date: send `X-Lenz-API-Version: 2026-10-11` to choose this shape. This reference describes `2026-10-11`. A field `2026-05-13` still sends under an older name is listed `deprecated` beside the current one, and where its shape differs, the operation names the `...Legacy` component it receives (`x-lenz-versions` on the response). Every response carries `X-Lenz-API-Version`, naming the version it is in. Which version a request gets without the header, and what changed: [https://lenz.io/docs/api-versions](https://lenz.io/docs/api-versions).\n\n## Install the SDK\n\n```bash\npip install lenz-io # Python\nnpm install lenz-io # Node / TypeScript\n```\n\nSource on GitHub: [lenz-io-python](https://github.com/lenzhq/lenz-io-python) \u00b7 [lenz-io-node](https://github.com/lenzhq/lenz-io-node).\n\n## The canonical integration: one /review call\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# Extract, assess, and verify the doubtful claims in one call (async, 2-4 min)\ndraft = \"The EU AI Act took effect in March 2024. It sorts AI systems into four risk tiers.\"\nreview = client.review_and_wait(text=draft)\nfor i in review.issues:\n print(i.verdict, i.claim, i.suggested_rewrite)\n```\n\nThe default policy: a quick verdict on every claim (up to 20), and the deep check on the rows whose quick verdict is `False`, `Mostly False` or `Mixed`, or whose confidence is `low`, up to five. Change it in `escalate`. To deep-check the rows the cap left out (`escalation.disposition == \"cap\"`), send them to `/verify/batch`. A resend with the same `Idempotency-Key` within 24 hours returns the same review; a new key is a new review.\n\n## The primitives, call by call: extract \u2192 assess \u2192 conditional verify\n\n```python\nfrom lenz_io import Lenz\nclient = Lenz(api_key=\"lenz_...\")\n\n# 1. Enumerate the factual claims in your model output (free)\n# extracted.claims lists every claim found, most check-worthy first:\n# always a list (one entry for one claim, [] for none).\n# Pass focus=\"market size and competitors\" to narrow the list;\n# status is \"no_match\" when the document has claims but none match.\nextracted = client.extract(text=llm_output)\nclaims = [c.claim for c in extracted.claims]\n\n# 2. Fast verdict in ONE call (typically 10-25s, 3-model panel): a list of up\n# to 20, one row per claim, in the order you sent them. extract finds up to\n# 100, most check-worthy first, so this checks the first 20.\nquick = client.assess(claims=claims[:20]).claims if claims else []\n\n# 3. Escalate the low-confidence ones to /verify in one batch (~90s, multi-model pipeline)\n# Rows we ran out of time on are free and worth resending; the rest of the\n# failed rows (no_checkable_claim, framing_failed) want a different input.\nretry = [c.claim for c in quick if c.failure and c.failure.code == \"timeout\"]\nquick += client.assess(claims=retry).claims if retry else []\ndoubtful = [{\"claim\": c.claim} for c in quick if c.status == \"completed\" and c.confidence == \"low\"]\nfor r in client.verify_batch_and_wait(claims=doubtful) if doubtful else []:\n if r.verification:\n print(r.verification.verdict, r.verification.lenz_score, r.verification.key_finding)\n```\n\n## How /verify works under the hood\n\nFrame \u2192 Collect Evidence \u2192 Debate (2 models, 2 rounds) \u2192 Adjudicate (three reviewers on models from different vendors, two more when they disagree) \u2192 Conclude. ~90 seconds wall-clock per claim.\n\n### While /verify is running\n\n`GET /verify/status/{task_id}` answers one of five shapes, keyed on `status`: `processing`, `needs_input`, `completed`, `failed`, and, after you cancel the run (see Stopping a run), `cancelled`. Every one echoes `task_id`, and only the fields belonging to that shape are present \u2014 read the ones for the branch you are in, and treat an absent key as absent rather than empty.\n\nA `processing` body carries `progress`:\n\n```json\n{\"status\": \"processing\", \"task_id\": \"\u2026\",\n \"progress\": {\"step\": \"research\", \"index\": 2, \"total\": 5,\n \"elapsed_seconds\": 42, \"poll_after_seconds\": 5}}\n```\n\n`step` is a closed set \u2014 `starting`, `framing`, `research`, `debate`, `adjudication`, `conclusion` \u2014 so it is safe to switch on. `index` is the 1-based stage position (0 while `starting`) out of `total`; read `total` off the response rather than hard-coding it. `poll_after_seconds` is how long to wait before looking again, and `elapsed_seconds` is how long the run has been going \u2014 a measurement, not an estimate of what is left.\n\nNote that `index` is stage **position**, not elapsed work: the stages are uneven, so a progress bar driven by it will sit on `research` for roughly half the run.\n\n`progress` is **advisory**. It says the work is pending and which stage it is on; it is not a place to read results from. The verdict arrives once, under `result`, when `status` is `completed`.\n\n### When /verify asks for input\n\n`/verify` is not only submit-and-poll. `GET /verify/status/{task_id}` can terminate at `needs_input` \u2014 the submitted text held several claims, or was too vague to check as written \u2014 and the task waits rather than guessing which claim you meant. Several claims in one sentence count the same as several sentences.\n\nResolve it with `POST /verify/{task_id}/select`, passing the exact wording of the claim(s) you want from the ones the status offered. Selection is **by text, not index**; anything that was not offered is rejected with a 422. Each selected claim fans out into its own pipeline, so the response carries one `task_id` per claim \u2014 poll each the way you would a fresh `/verify`.\n\nClaims returned by `/extract` skip this entirely: submitted verbatim, they are never bounced back for rephrasing.\n\n### Covered verification\n\nEvery verification a paid Pro or Scale account runs is put in front of a gate. The ones that qualify carry a contractual warranty, and every result tells you whether it qualified \u2014 a `coverage` block on the completed `/verify/status/{task_id}` body, on `GET /verifications/{id}` and on the `verification.completed` webhook.\n\nIt is on by default, with no request field to set; an account on Pro or Scale can turn certificates off on the API credentials page, for checks submitted from then on (reason `account`; a verification that already carries a certificate keeps it).\n\n`status` is `covered`, `pending_timestamp` or `uncovered`; `reasons` says why not, in a closed seven-value vocabulary. **If you publish automatically, key on the `certificate.timestamped` webhook, not `verification.completed`** \u2014 cover requires the qualified timestamp to precede what you publish, and `completed` fires while the qualified timestamp is still pending.\n\nFull wire format, the reason vocabulary and the offline verifier: [lenz.io/docs/coverage](https://lenz.io/docs/coverage). What is covered and what is not: [lenz.io/warranty](https://lenz.io/warranty).\n\n### When /verify fails\n\nA `failed` status (and the `verification.failed` webhook) carries two axes. `failure_reason` says **where** the pipeline stopped (`research_empty`, `conclusion_failed`, `task_stuck`, \u2026); on the webhook the same value rides in `error`. `failure_class` says **why**, and `retryable` is derived from it:\n\n| `failure_class` | Meaning | `retryable` |\n|---|---|---|\n| `upstream_unavailable` | Model or search providers were rate-limited or down, or the task was never run \u2014 resubmit after a short wait | `true` |\n| `insufficient_evidence` | We looked; the sources are not there | `false` |\n| `invalid_input` | Not a checkable claim, an unreadable URL | `false` |\n| `cancelled` | You stopped it with `POST /verify/{task_id}/cancel`; not charged | `false` |\n| `internal` | Anything else \u2014 retrying the same input will not help | `false` |\n\nBranch on `retryable`, not on `failure_reason`: the same reason can mean either. `failure_class` is a **closed set** \u2014 these five values are stable identifiers. `failure_reason` is an open, informational set (`task_error`, `task_stuck`, `not_a_claim`, \u2026 will appear). A failed body also carries `docs_url`, pointing at the explanation of its class. A failed `/verify` is not charged.\n\nIn the next API version a cancelled run is not a failure: `GET /verify/status/{task_id}` answers `{\"status\": \"cancelled\", \"task_id\": \"\u2026\"}`, and a `webhook_url` receives `verification.cancelled`. A request made in 2026-05-13 keeps `status: \"failed\"` with `failure_class: \"cancelled\"` and receives `verification.failed`.\n\n### Stopping a run\n\n`POST /verify/{task_id}/cancel` stops a verification that has not finished. A cancelled run is not charged and saves no verification; model work already under way is not billed to you, and the run stops at its next step. The call is safe to repeat and answers 200 in every state: `cancelled: true` with `status: \"cancelled\"`, or `cancelled: false` with `status: \"completed\"` (it finished first and was charged as usual) or `\"failed\"`. It needs no `Idempotency-Key`. An unknown task, another account's task and a task not submitted through the API answer 404 `not_found`; a task that is a `/review`'s deep check answers 409 `use_review_cancel`. Each item of a `/verify/batch` has its own `task_id`, so cancel per task; there is no call for a whole batch. `POST /reviews/{review_id}/cancel` and `POST /citechecks/{citecheck_id}/cancel` stop everything in a review or a citation check. They refund the quick checks not yet served and the citations not yet checked, and keep what was delivered charged.\n\nOne of those reasons comes from framing rather than from a pipeline step and carries `failure_class: invalid_input`: `not_a_claim` (the input is not a checkable statement, or is too vague to have a checkable reading). A vague input that has a likely reading is verified on it, and the result's `claim` is that reading. On the webhook the same value rides in `error`, as an ordinary `verification.failed` delivery; before 2026-09-03 it fired nothing at all, so a task submitted with a `webhook_url` could terminate in silence.\n\n### What may change without notice\n\nNew keys may be added to status and webhook bodies at any time \u2014 ignore keys you do not recognise. Fields documented as **advisory** \u2014 `progress` and everything inside it \u2014 may also change or disappear without a version bump. The verdict fields under `result` may not: those change only with a versioned, announced release.\n\n## Common patterns\n\n- **Runtime sync UX**: `POST /assess` on the user-facing path; escalate `confidence == \"low\"` claims to `/verify` in the background.\n- **Runtime webhook**: `POST /verify` with `webhook_url`. Lenz POSTs the typed payload when done. Best for document pipelines.\n- **Whole drafts**: `POST /review` with `webhook_url`; handle `review.completed` (a `ReviewWebhookPayload`, deduped on `event_id`) and read `review.issues[]` and `review.citation_issues[]`.\n- **CI / pre-release**: `/extract` + batch `/verify` your golden set on every deploy. The free `/extract` tier covers most of this. Claims returned by `/extract` verify cleanly \u2014 submitted verbatim to `/verify`, they are never bounced back for rephrasing.\n- **Incident triage**: paste the offending output into `/extract`, then `/verify` the wrong-looking claims. Get a citation trail to send back to the customer.\n\n## What's in the response\n\nUnified vocabulary across every claim-shaped response:\n\n- **`claim`** \u2014 the framed claim text.\n- **`verdict`** \u2014 `\"True\" | \"Mostly True\" | \"Mixed\" | \"Mostly False\" | \"False\" | \"Error\"`.\n- **`confidence`** \u2014 `\"high\" | \"medium\" | \"low\"`.\n- **`lenz_score`** \u2014 1\u201310 integer score (deep payloads).\n- **`key_finding`** \u2014 one declarative sentence stating the most important fact the analysis established. For a false claim it states the CORRECTED fact rather than restating the claim, so render it next to the verdict label, never as a standalone headline. `\"\"` on older verifications predating the field.\n- **`suggested_rewrite`** \u2014 a suggested rewrite of `claim` that the verification's findings support. It has not been verified itself: before using it, review it or run it through `/verify`. `null` for a true claim, when no correction is established, and on older verifications. On every verification, single or listed (`GET /verifications/{id}`, `/verify/status`, the `verification.completed` webhook, `GET /verifications`, `GET /library`). On `/assess` rows too when the request sets `suggest_rewrite`: written from the quick check's reasoning for a claim it found False or Mostly False with high confidence, at no extra credit; `null` otherwise, and on every row when not requested.\n- **`sources`** \u2014 evidence with title, URL and snippet: the sentence(s) quoted from the page, verbatim and in the page's own language (`/verify` only).\n- **`audit`** \u2014 adjudication reasoning, debate transcript, panel agreement (`/verify` only).\n\n## How the API is organized\n\n- **Extract** \u2014 pull verifiable claims out of text.\n- **Assess** \u2014 fast 3-model verdict.\n- **Verify** \u2014 deep multi-model pipeline: submit one claim or a batch, poll status, and resolve a `needs_input` interrupt by selecting claims.\n- **Verifications** \u2014 list, fetch, delete, and toggle visibility on stored verifications.\n- **Library** \u2014 browse the public catalog. No API key needed.\n- **Ask** \u2014 follow-up questions, 1 credit each.\n- **Account** \u2014 your credit balance and the price list.\n\n## Credits\n\nOne pool per account funds every billable call, at a fixed weight:\n\n| Endpoint | Credits |\n|---|---|\n| `POST /verify` (and `/verify/batch`, `/select`) | 10 per claim |\n| `POST /verify` with `depth: \"low\"` | 5 per claim |\n| `POST /assess` | 1 per claim \u2014 per row that carries a verdict; error rows and rows answered from the cache are free |\n| `POST /ask` | 1 |\n| `POST /extract` | 0 \u2014 free, fair-use capped |\n\n`depth: \"low\"` caps research breadth \u2014 fewer discovery queries, a hard extraction ceiling, no recovery tiers on the happy path \u2014 while every reasoning step runs unchanged. It is not a model downgrade. You are charged for the depth you REQUESTED. A `/verify/batch` may mix depths per item and is billed per item.\n\nAn answer served from the last hour's cache is free: a `/verify`, `/assess` or `/review` claim that gets back a verdict checked in the last hour costs nothing, so a tool that resends a request is not charged twice. The exception is a `/verify` that issues you a new warranty certificate, charged at the depth you requested.\n\n`GET /me/usage` reports the balance under `credits` and the weights under `costs`, with parameter-dependent prices such as `depth` nested under `cost_options`. In version `2026-05-13` the per-capability blocks beside them (`verify`, `assess`, `ask`) are **projections** of that one balance into each capability's own unit \u2014 how many of those calls the remaining credits would buy \u2014 not separate allowances. Spending on any one of them moves all of them.\n\n## Authentication\n\n1. [Create a Lenz account](https://lenz.io/auth).\n2. Generate an API key on the [API credentials](https://lenz.io/api-credentials) page.\n3. Send it on every request:\n ```\n Authorization: Bearer lenz_...\n ```\n\nOr skip steps 1\u20132: **[lenz.io/setup](https://lenz.io/setup)** issues a test key and generates setup instructions for your environment \u2014 MCP client, SDK or plain HTTP \u2014 that you paste into your coding agent to do the wiring.\n\n### One balance, whichever door you come through\n\nLenz also runs a remote MCP server at `https://lenz.io/mcp`, so an agent can fact-check inside a conversation. It is **not** part of this surface \u2014 there are no MCP endpoints below, and its tools are documented at [lenz.io/integrations/mcp-server](https://lenz.io/integrations/mcp-server).\n\nIt matters here for one reason: that server is a client of this API. It takes the same `lenz_...` key and forwards it to the endpoints below, so MCP traffic spends the same credits, counts against the same rate limits, and shows up in `GET /me/usage` \u2014 there is no separate MCP quota to check, and a 402 can just as easily be an agent as your own code.\n\n## Idempotency\n\nSend an `Idempotency-Key` header on `POST /extract`, `POST /assess`, `POST /verify`, `POST /verify/batch`, `POST /verify/{task_id}/select`, `POST /review`, `POST /citecheck` and `POST /ask/{verification_id}` so retries after a network drop do not spawn duplicate tasks or double-debit quota. The server caches the response body and returns it on replay; a re-used key with a different body returns 422. The SDKs generate a key for `extract`, `assess`, `select`, `verify`, `verify_and_wait`, `verify_batch`, `verify_batch_and_wait`, `ask.send`, `review` and `citecheck` and reuse it across their own retries (lenz-io 3.0; 2.x sent a key on `verify_batch` and `ask.send` only when you passed `idempotency_key` yourself). An error from a keyed call carries the key (`idempotency_key` in Python, `idempotencyKey` in Node): resend with that same key, never as a plain new call, which sends a new key and can run the work twice.\n\nOn `/ask` a key also keeps the retry out of the conversation: without one, a repeated question is a second turn, a second credit and a second answer that the next turn reads as context. A retry that lands while the first call is still running gets 409 rather than the reply \u2014 nothing is finalized yet to replay \u2014 so treat it as work already in progress and retry the same key once the first call has had time to finish. `GET /ask/{verification_id}` returns the message history, but carries no request id, so it can only tell you which answer was yours when one question is in flight at a time. lenz-io 3.0 sends a fresh key on every `ask.send` call and reuses it across the retries inside that call, so a retried question is not asked twice. Asking the same question again is a new call with a new key, and is asked again, because a repeated question is usually deliberate; pin the key to make a retry from another process mean \"the same question, once\": `ask.send(..., idempotency_key=...)` in Python, `ask.send(id, { message, idempotencyKey })` in Node. The n8n node's Ask operation sends a key scoped to the workflow execution, so a retry replays rather than re-asks. The Zapier app sends none on ask.\n\n## Errors\n\nThe status code says what to do next; the body says what happened. Every response carries an `X-Request-ID` header \u2014 quote it on a support ticket. lenz-io 3.0 reads the last column for you: `retryable` is set on every SDK error (true for a 429, a 5xx, a dropped connection, a request timeout and a 409 that means \"not yet\"; false for any other 4xx), so branch on it instead of the status.\n\n| Status | Condition | Retry? |\n|---|---|---|\n| 401 | Missing, malformed or unknown key | After fixing credentials |\n| 402 | Out of credits, or the plan is the limit | No \u2014 billing action |\n| 403 | Authenticated but not allowed (private verification, IP block) | No |\n| 409 | A request with the same `Idempotency-Key` is still running, or the work the call names is not in the state it needs (`no_selection_pending`, `verification_not_ready`, `verification_failed`, `use_review_cancel`) | Same-key conflict: yes, with the same key. `verification_not_ready`: poll, or choose the claims when it needs input. `no_selection_pending`: poll the status first. `use_review_cancel`: cancel the review instead |\n| 422 | Malformed input | No |\n| 429 | Rate limit: the per-account `/extract` daily cap, or too many reviews or citation checks running (`review_in_flight`, `citecheck_in_flight`) | Yes, after `Retry-After` |\n| 502 | A sync step (`/assess`, `/extract`, `/ask`) failed deterministically \u2014 a schema or parse failure (`/ask`: `ask_failed`) | No \u2014 same input, same outcome |\n| 503 | Our model providers were rate-limited or down for the step and every fallback was exhausted (`code: upstream_unavailable`) or the pipeline has no capacity (`code: capacity`) | Yes, after `Retry-After` (body `retry_after` too) |\n\n**402** carries `detail`, `code` (always `no_credits`), `upgrade_url`, `doc_url`, `wall_id`, `cost` (what the rejected call would have taken), and \u2014 when resolvable \u2014 `remaining`, `credits_remaining` and `resets_at`. `remaining` and `requested` are in the *capability's* unit (verifications, assesses); `credits_remaining` and `cost` are in credits, so a client can tell \"not enough for a 10-credit verify\" from \"empty\". The resolvable fields are omitted rather than sent as `null`, so an absent key means \"unknown\", not \"zero\".\n\n`wall_id` identifies this specific rejection and is already appended to `upgrade_url`, so sending a user to that URL is all that is needed \u2014 it exists as its own field only for clients that build their own upgrade link. Treat `upgrade_url` as opaque and follow it whole rather than matching it against a fixed string.\n\nThe `code` values are deliberately plan-agnostic: they name the condition, never the plan that would fix it. Send users to `upgrade_url` rather than mapping codes to plan names \u2014 the set of plans changes, the conditions do not.\n\nAn empty balance is deliberately **not** a 429: a 429 tells every client to retry, and an empty balance never succeeds on retry. **429** carries `code`, `limit`, `reset_in_seconds`, `upgrade_url` and the `Retry-After` header. The in-flight 429 of `/review` and `/citecheck` (`review_in_flight`, `citecheck_in_flight`) carries `retry_after_seconds` and `Retry-After` instead. The daily cap is the same on every plan; only a key Lenz has marked unlimited for `/extract` bypasses it.\n\n**Migrating (August 2026).** These rejections were 403, which the SDKs map to their auth error; they are now 402, which maps to the quota error \u2014 not a subclass. If you catch the auth error to handle an empty balance, that branch stops firing. This reaches you on every SDK version, including ones released before the change. `detail` strings are unchanged. Full reference: [lenz.io/docs/errors](https://lenz.io/docs/errors#quota).\n\n## Webhooks\n\nOn `POST /verify` and `POST /review` you may supply a `webhook_url`. We POST an HMAC-SHA256-signed JSON payload to it when the pipeline terminates. Signature header is `X-Lenz-Signature: sha256=`. Verify with your webhook secret (generate + rotate on [API credentials](https://lenz.io/api-credentials)). Retries: 10s, 60s, 600s (3 retries after the initial delivery, 4 attempts total). The SDKs ship a `LenzWebhooks` helper that verifies signatures and parses the payload into typed events. Events: `verification.completed`, `verification.failed`, `verification.cancelled`, `certificate.timestamped`, and for a review `review.completed` / `review.failed` / `review.cancelled` and for a citation check `citecheck.completed` / `citecheck.failed` / `citecheck.cancelled` (switch on `event`; a review's own deep checks fire no `verification.*` events). The `*.cancelled` events fire once, when you cancel, in the next API version; a submission made in 2026-05-13 receives the matching `*.failed` event with `failure_class: \"cancelled\"` instead.\n\nFor commercial use, volume pricing, or onboarding support, [get in touch](https://lenz.io/contact).", "termsOfService": "https://lenz.io/terms" }, "paths": { @@ -461,11 +461,11 @@ "x-codeSamples": [ { "lang": "Python", - "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nbatch = client.verify_batch(claims=[\n {\"text\": \"The Eiffel Tower is in Berlin.\"},\n {\"text\": \"Water boils at 100\u00b0C at sea level.\"},\n])\nfor item in batch.items:\n print(item.task_id, item.claim)\n" + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nbatch = client.verify_batch(claims=[\n {\"claim\": \"The Eiffel Tower is in Berlin.\"},\n {\"claim\": \"Water boils at 100\u00b0C at sea level.\"},\n])\nfor item in batch.items:\n print(item.task_id, item.claim)\n" }, { "lang": "TypeScript", - "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst batch = await client.verifyBatch({\n claims: [\n { text: \"The Eiffel Tower is in Berlin.\" },\n { text: \"Water boils at 100\u00b0C at sea level.\" },\n ],\n});\nbatch.items.forEach(i => console.log(i.task_id, i.claim));\n" + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst batch = await client.verifyBatch({\n claims: [\n { claim: \"The Eiffel Tower is in Berlin.\" },\n { claim: \"Water boils at 100\u00b0C at sea level.\" },\n ],\n});\nbatch.items.forEach(i => console.log(i.task_id, i.claim));\n" } ] } @@ -572,11 +572,11 @@ "x-codeSamples": [ { "lang": "Python", - "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nout = client.assess(claim=\"Coffee causes cancer and the earth is flat.\")\n# Add language=\"es\" for Spanish verdict text; verdict labels stay English.\nfor c in out.claims:\n print(c.claim, \"\u2192\", c.verdict, c.confidence)\n if c.rationale:\n print(\" \", c.rationale)\n\n# Several claims at once: one row per item, same order (up to 20).\nrows = client.assess(claims=[\"Coffee causes cancer.\", \"The earth is flat.\"]).claims\nfor c in rows:\n print(c.failure.hint if c.status == \"failed\" else c.verdict)\n if c.dissent:\n print(\" One reviewer disagreed:\", c.dissent)\n" + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nout = client.assess(claim=\"Coffee causes cancer and the earth is flat.\")\n# Add language=\"es\" for Spanish verdict text; verdict labels stay English.\nfor c in out.claims:\n print(c.claim, \"\u2192\", c.verdict, c.confidence)\n if c.rationale:\n print(\" \", c.rationale)\n\n# Several claims at once: one row per item, same order (up to 20).\nrows = client.assess(claims=[\"Coffee causes cancer.\", \"The earth is flat.\"]).claims\nfor c in rows:\n if c.failure: # status == \"failed\": no verdict, nothing charged\n print(\"No verdict:\", c.failure.code, c.failure.hint)\n else:\n print(c.verdict, c.confidence)\n if c.dissent:\n print(\" One reviewer disagreed:\", c.dissent)\n" }, { "lang": "TypeScript", - "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst out = await client.assess({\n claim: \"Coffee causes cancer and the earth is flat.\",\n});\nout.claims.forEach(c => {\n console.log(c.claim, \"\u2192\", c.verdict, c.confidence);\n if (c.rationale) console.log(\" \", c.rationale);\n});\n\n// Several claims at once: one row per item, same order (up to 20).\nconst rows = (await client.assess({\n claims: [\"Coffee causes cancer.\", \"The earth is flat.\"],\n})).claims;\nrows.forEach(c => {\n console.log(c.status === \"failed\" ? c.failure?.hint : c.verdict);\n if (c.dissent) console.log(\" One reviewer disagreed:\", c.dissent);\n});\n" + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst out = await client.assess({\n claim: \"Coffee causes cancer and the earth is flat.\",\n});\nout.claims.forEach(c => {\n console.log(c.claim, \"\u2192\", c.verdict, c.confidence);\n if (c.rationale) console.log(\" \", c.rationale);\n});\n\n// Several claims at once: one row per item, same order (up to 20).\nconst rows = (await client.assess({\n claims: [\"Coffee causes cancer.\", \"The earth is flat.\"],\n})).claims;\nrows.forEach(c => {\n // status \"failed\": no verdict, nothing charged\n if (c.status === \"failed\") console.log(\"No verdict:\", c.failure?.code, c.failure?.hint);\n else console.log(c.verdict, c.confidence);\n if (c.dissent) console.log(\" One reviewer disagreed:\", c.dissent);\n});\n" } ] } @@ -880,11 +880,11 @@ "x-codeSamples": [ { "lang": "Python", - "source": "from lenz_io import Lenz\n\n# typed progress needs lenz-io >= 2.11.0\nclient = Lenz(api_key=\"lenz_...\")\nstatus = client.get_status(\"tsk_abc123\")\n\n# \"processing\" | \"needs_input\" | \"completed\" | \"failed\"\nif status.status == \"processing\":\n p = status.progress\n print(f\"{p.step} \u2014 step {p.index} of {p.total}\")\nelif status.status == \"completed\":\n print(status.result.verdict, status.result.key_finding)\n" + "source": "from lenz_io import Lenz\n\n# typed progress needs lenz-io >= 2.11.0\nclient = Lenz(api_key=\"lenz_...\")\nstatus = client.get_status(\"tsk_abc123\")\n\n# \"processing\" | \"needs_input\" | \"completed\" | \"failed\" | \"cancelled\"\nif status.status == \"processing\" and status.progress:\n p = status.progress\n print(f\"{p.step} \u2014 step {p.index} of {p.total}\")\nelif status.status == \"completed\" and status.result:\n print(status.result.verdict, status.result.key_finding)\nelif status.failure: # failed or cancelled\n print(status.status, status.failure.code, status.failure.retryable)\n" }, { "lang": "TypeScript", - "source": "// typed progress needs lenz-io >= 2.11.0\nimport { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst s = await client.getStatus(\"tsk_abc123\");\n\nif (s.status === \"processing\" && s.progress) {\n const { step, index, total } = s.progress;\n console.log(`${step} \u2014 step ${index} of ${total}`);\n} else if (s.status === \"completed\") {\n console.log(s.result!.verdict, s.result!.key_finding);\n}\n" + "source": "// typed progress needs lenz-io >= 2.11.0\nimport { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst s = await client.getStatus(\"tsk_abc123\");\n\nif (s.status === \"processing\" && s.progress) {\n const { step, index, total } = s.progress;\n console.log(`${step} \u2014 step ${index} of ${total}`);\n} else if (s.status === \"completed\") {\n console.log(s.result!.verdict, s.result!.key_finding);\n} else if (s.failure) {\n // \"failed\" or \"cancelled\"\n console.log(s.status, s.failure.code, s.failure.retryable);\n}\n" } ] } @@ -963,6 +963,16 @@ { "APIKeyBearerAuth": [] } + ], + "x-codeSamples": [ + { + "lang": "Python", + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\nresult = client.cancel(\"tsk_abc123\") # lenz-io >= 3.0.0\n# cancelled=True: stopped (by this call or an earlier one), not charged.\n# cancelled=False: it had ended; status is \"completed\" or \"failed\",\n# or \"needs_input\" when select already resolved it: cancel each task id\n# select returned (those keep running).\nprint(result.cancelled, result.status)\n" + }, + { + "lang": "TypeScript", + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst result = await client.cancel(\"tsk_abc123\"); // lenz-io >= 3.0.0\n// cancelled: true = stopped (by this call or an earlier one), not charged.\n// cancelled: false = it had ended; status is \"completed\" or \"failed\",\n// or \"needs_input\" when select already resolved it: cancel each task id\n// select returned (those keep running).\nconsole.log(result.cancelled, result.status);\n" + } ] } }, @@ -1023,7 +1033,7 @@ } } }, - "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\nThe current shape is ``credits``, ``costs``, ``cost_options``, ``extract``,\n``plan``, ``plan_label`` and ``has_webhook_secret``. Callers on API version\n``2026-05-13`` also get the deprecated fields below, which are kept for\nthem: ``credits.bonus``, ``quota_resets_at`` (the same\nas ``credits.resets_at``) and the ``verify`` / ``ask`` / ``assess`` blocks.\n\n``credits.bonus`` (``2026-05-13`` only) is the deprecated old name of\n``credits.extra``, the same number. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15 (#664), while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated** (``2026-05-13`` only). ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nRead ``credits`` and ``costs`` instead; the blocks and the per-block\n``credits`` alias stay for ``2026-05-13`` callers. Derive them::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias, kept for the same callers.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.\n\n**Versions.** 200: `2026-10-11` receives `MeUsageOut`, `2026-05-13` receives `MeUsageOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", + "description": "Return the calling account's credit balance and what it can still buy.\n\n**``credits`` is the balance.** One pool funds every billable capability,\nat the weights in ``costs`` \u2014 today ``verify`` 10, ``assess`` 1, ``ask`` 1.\nIt has two buckets: the monthly allowance for the current ``plan``, which\nresets at ``resets_at``, and non-expiring ``extra`` credits from grants and\ntop-ups, spent only once the allowance is gone. ``remaining`` is the sum,\nand is what a call is actually checked against.\n\nThe current shape is ``credits``, ``costs``, ``cost_options``, ``extract``,\n``plan``, ``plan_label`` and ``has_webhook_secret``. Callers on API version\n``2026-05-13`` also get the deprecated fields below, which are kept for\nthem: ``credits.bonus``, ``quota_resets_at`` (the same\nas ``credits.resets_at``) and the ``verify`` / ``ask`` / ``assess`` blocks.\n\n``credits.bonus`` (``2026-05-13`` only) is the deprecated old name of\n``credits.extra``, the same number. Read ``extra``.\n\n``plan`` is the stable slug to branch on; ``plan_label`` is the same tier\nas display copy (\"Pro\"). Two fields rather than one humanised\nstring, so a reworded label cannot break a client comparing tiers.\nThe Pro plan's ``plan`` is ``pro``; it read ``developer`` before\n2026-09-15, while ``plan_label`` already read \"Pro\".\n\n**The per-capability blocks are deprecated** (``2026-05-13`` only). ``verify`` / ``ask`` /\n``assess`` are **projections** of that one pool into each capability's own\nunit \u2014 \"how many of these could I still make\" \u2014 not separate allowances.\nSpending on one moves all of them, and they are floor divisions, so a\nverify block ticks once per 10 credits spent anywhere.\n\nRead ``credits`` and ``costs`` instead; the blocks and the per-block\n``credits`` alias stay for ``2026-05-13`` callers. Derive them::\n\n remaining = credits.remaining // costs[capability]\n total = credits.total // costs[capability]\n used = total - remaining\n\nTwo capabilities at the same price emit identical blocks \u2014 ``ask`` and\n``assess`` are both 1 credit \u2014 which is the clearest sign the shape\ndescribes an allowance that no longer exists.\n\n``bonus`` in each block is ``credits.extra`` in that capability's unit. The\nolder key ``credits`` is its deprecated alias, kept for the same callers.\n\n``costs`` is one entry per capability at its DEFAULT price. Prices that\ndepend on a request parameter live in ``cost_options``, nested capability\n\u2192 parameter \u2192 value \u2014 ``{\"verify\": {\"depth\": {\"standard\": 10, \"low\": 5}}}``.\nTwo maps rather than a flat one with a ``verify_low`` key, so a future\ntuning parameter nests instead of adding an entry that reads like a fifth\ncapability. Every capability in ``cost_options`` also appears in ``costs``\nat its default, so reading only ``costs`` is always correct.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and is bounded by\na per-account daily fair-use cap instead, reported separately here and\nrejected with 429 rather than 402.\n\n**Versions.** 200: `2026-10-11` receives `MeUsageOut`, `2026-05-13` receives `MeUsageOutLegacy`; default: `2026-10-11` receives `ErrorOut`, `2026-05-13` receives `ErrorOutLegacy`.", "tags": [ "Account" ], @@ -2313,6 +2323,16 @@ { "APIKeyBearerAuth": [] } + ], + "x-codeSamples": [ + { + "lang": "Python", + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\n# Stops the review, its deep checks and its citation checks (lenz-io >= 3.0.0).\nreview = client.cancel_review(\"a1b2c3d4\") # the full review, as get_review reads it\nprint(review.status, review.credits.charged) # \"cancelled\", what it delivered cost\n" + }, + { + "lang": "TypeScript", + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\n// Stops the review, its deep checks and its citation checks (lenz-io >= 3.0.0).\nconst review = await client.cancelReview(\"a1b2c3d4\"); // as getReview reads it\nconsole.log(review.status, review.credits.charged);\n" + } ] } }, @@ -2782,6 +2802,16 @@ { "APIKeyBearerAuth": [] } + ], + "x-codeSamples": [ + { + "lang": "Python", + "source": "from lenz_io import Lenz\n\nclient = Lenz(api_key=\"lenz_...\")\ncheck = client.cancel_citecheck(\"12bbbf65\") # lenz-io >= 3.0.0; as get_citecheck reads it\nprint(check.status, check.credits.charged) # citations not checked are refunded\n" + }, + { + "lang": "TypeScript", + "source": "import { Lenz } from \"lenz-io\";\n\nconst client = new Lenz({ apiKey: \"lenz_...\" });\nconst check = await client.cancelCitecheck(\"12bbbf65\"); // lenz-io >= 3.0.0\nconsole.log(check.status, check.credits.charged);\n" + } ] } } @@ -2789,7 +2819,7 @@ "components": { "schemas": { "EntityRef": { - "description": "An entity (person, place, organization, concept) referenced in the\nsubmitted claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42`` for Douglas Adams)\nwhen Lenz resolved the entity against its internal catalog; ``null``\notherwise. Customers can join on ``qid`` to their own Wikidata-indexed\ncorpus, or hit the Wikidata API directly for richer metadata.", + "description": "An entity (person, place, organization, concept) referenced in the\nsubmitted claim.\n\n``qid`` is the Wikidata Q identifier (e.g. ``Q42`` for Douglas Adams)\nwhen Lenz resolved the entity against its own entity catalog; ``null``\notherwise. Customers can join on ``qid`` to their own Wikidata-indexed\ncorpus, or hit the Wikidata API directly for richer metadata.", "properties": { "name": { "title": "Name", @@ -3895,7 +3925,7 @@ "type": "object" }, "CoverageOut": { - "description": "Whether this verdict carries a warranty, and if not, why not.\n\nPresent only when covered verification is enabled. It describes the pair\n(this account, this analysis), not the analysis alone: two accounts can\nhold two certificates over one cached verdict, so the block one customer\nsees is never the block another sees.\n\n``reasons`` is empty exactly when ``status`` is not ``uncovered``. The\nvocabulary is closed and deliberately smaller than the internal gate's:\n``plan`` and ``depth`` are actionable, ``account`` means the account\nturned certificates off (Pro and Scale), ``verdict`` is a product rule\ncallers design around, ``quality`` covers everything a caller can neither\nact on nor define, and ``withdrawn`` / ``issue_failed`` are statements\nabout Lenz rather than about the caller's claim.", + "description": "Whether this verdict carries a warranty, and if not, why not.\n\nPresent only when covered verification is enabled. It describes the pair\n(this account, this analysis), not the analysis alone: two accounts can\nhold two certificates over one cached verdict, so the block one customer\nsees is never the block another sees.\n\n``reasons`` is empty exactly when ``status`` is not ``uncovered``. The\nvocabulary is closed and deliberately smaller than the set of rules\nbehind the decision: ``plan`` and ``depth`` are actionable, ``account``\nmeans the account turned certificates off (Pro and Scale), ``verdict`` is\na product rule callers design around, ``quality`` covers everything a\ncaller can neither act on nor define, and ``withdrawn`` / ``issue_failed``\nare statements about Lenz rather than about the caller's claim.", "properties": { "status": { "default": "uncovered", @@ -4270,7 +4300,7 @@ "type": "object" }, "MeUsageOut": { - "description": "The calling account's credit balance, and what it can still buy.\n\n``credits`` is the balance and ``costs`` is the price list. Those two,\nplus ``cost_options``, are the whole contract for a new integration. The\ncurrent shape is ``credits``, ``costs``, ``cost_options``, ``extract``,\n``plan``, ``plan_label`` and ``has_webhook_secret``.\n\nCallers on API version ``2026-05-13`` also get the DEPRECATED fields, kept\nfor them: the ``verify`` / ``ask`` / ``assess`` blocks\n(projections of that same pool into each capability's unit) with their\n``credits`` alias, ``credits.bonus`` and ``quota_resets_at``.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and carries a\nper-account daily fair-use cap instead; it rejects with 429, not 402.\n\nThe Pro plan's ``plan`` is ``pro`` since 2026-09-15; it was ``developer``\nbefore (#664). ``plan_label`` reads \"Pro\" either way.", + "description": "The calling account's credit balance, and what it can still buy.\n\n``credits`` is the balance and ``costs`` is the price list. Those two,\nplus ``cost_options``, are the whole contract for a new integration. The\ncurrent shape is ``credits``, ``costs``, ``cost_options``, ``extract``,\n``plan``, ``plan_label`` and ``has_webhook_secret``.\n\nCallers on API version ``2026-05-13`` also get the DEPRECATED fields, kept\nfor them: the ``verify`` / ``ask`` / ``assess`` blocks\n(projections of that same pool into each capability's unit) with their\n``credits`` alias, ``credits.bonus`` and ``quota_resets_at``.\n\n``extract`` is free at the pool (``costs.extract`` is 0) and carries a\nper-account daily fair-use cap instead; it rejects with 429, not 402.\n\nThe Pro plan's ``plan`` is ``pro`` since 2026-09-15; it was ``developer``\nbefore. ``plan_label`` reads \"Pro\" either way.", "properties": { "plan": { "default": "free", @@ -8072,7 +8102,7 @@ "type": "object" }, "WebhookPayload": { - "description": "Body the customer's endpoint receives for a verification event.\n\nOne envelope for every event (plan no-claim-unification vote 20):\n``event``, ``event_id`` (the same on every retry: deduplicate on it),\n``task_id`` (pollable on ``GET /verify/status/{task_id}``),\n``verification_id`` once the verification exists, ``status``, and the\nbody polling returns at that moment under ``verification``. Keys a payload\ndoes not carry are absent, not null.\n\nAlways signed with HMAC-SHA256 over the raw body bytes; signature\nrides in the ``X-Lenz-Signature: sha256=`` header, and the version\nthe body was built in in ``X-Lenz-API-Version``. A webhook follows the\nversion of the request that asked for it; version ``2026-05-13`` sends\nthe earlier flat body (``result`` / ``needs_input`` / ``error`` beside\n``task_id``, no ``event_id``).\n\nThe ``attempt`` field is 1-indexed and includes the initial delivery,\nso values run 1 through 4 (initial + 3 retries).", + "description": "Body the customer's endpoint receives for a verification event.\n\nOne envelope for every event: ``event``, ``event_id`` (the same on every\nretry: deduplicate on it), ``task_id`` (pollable on\n``GET /verify/status/{task_id}``), ``verification_id`` once the\nverification exists, ``status``, and the body polling returns at that\nmoment under ``verification``. Keys a payload does not carry are absent,\nnot null.\n\nAlways signed with HMAC-SHA256 over the raw body bytes; signature\nrides in the ``X-Lenz-Signature: sha256=`` header, and the version\nthe body was built in in ``X-Lenz-API-Version``. A webhook follows the\nversion of the request that asked for it; version ``2026-05-13`` sends\nthe earlier flat body (``result`` / ``needs_input`` / ``error`` beside\n``task_id``, no ``event_id``).\n\nThe ``attempt`` field is 1-indexed and includes the initial delivery,\nso values run 1 through 4 (initial + 3 retries).", "properties": { "event": { "title": "Event", @@ -8631,7 +8661,7 @@ "description": "The signed warranty certificate of a covered verification, as issued. Its format and the offline check are at https://lenz.io/docs/coverage." }, "VerificationListItemOutLegacy": { - "description": "The ``2026-05-13`` list item: ``modified_at`` (set only when completed\non a later UTC day) where ``VerificationListItemOut`` has\n``completed_at``. Served by no route schema; it documents the shape\n``lenz.api.downgrade.verify`` restores for a legacy caller.", + "description": "The ``2026-05-13`` list item: ``modified_at`` (set only when completed\non a later UTC day) where ``VerificationListItemOut`` has\n``completed_at``. It is the shape a caller on that version receives.", "properties": { "verification_id": { "title": "Verification Id", @@ -8974,7 +9004,7 @@ "type": "object" }, "WebhookPayloadLegacy": { - "description": "The ``2026-05-13`` body of a verification event, sent for a submission\nmade in that version (K5): flat, no ``event_id``, the failure code in\n``error``. Produced by ``lenz.api.downgrade.webhooks`` from the canonical\n``WebhookPayload``; never used to build one (kept to describe the older\nversion's component).", + "description": "The ``2026-05-13`` body of a verification event, sent for a submission\nmade in that version: flat, no ``event_id``, the failure code in\n``error``. The canonical ``WebhookPayload`` carries the same facts in\nits own shape.", "properties": { "event": { "title": "Event", From 010f40c11b000de489f282f7c950bcc58c44c480 Mon Sep 17 00:00:00 2001 From: Kosta Date: Sat, 10 Oct 2026 01:27:05 +0300 Subject: [PATCH 67/67] style: satisfy ruff 0.17 (None last in unions; README code formatting) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011uf7t6njK4RrozsjwmNZTD --- README.md | 20 ++++++++++++-------- src/lenz_io/client.py | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 122fa25..4069a82 100644 --- a/README.md +++ b/README.md @@ -959,16 +959,16 @@ stopped. Each has its own method, and each answers 200 whatever the state of the run, so losing a race is not an error: ```python -result = client.cancel("tsk_abc123") # a verification -> CancelResult +result = client.cancel("tsk_abc123") # a verification -> CancelResult if result.cancelled: - print("stopped:", result.status) # "cancelled" + print("stopped:", result.status) # "cancelled" else: - print("already ended:", result.status) # "completed" or "failed" + print("already ended:", result.status) # "completed" or "failed" -review = client.cancel_review("d6b2bd72") # the full view, like get_review -print(review.status, review.credits.charged) # "cancelled", what it cost +review = client.cancel_review("d6b2bd72") # the full view, like get_review +print(review.status, review.credits.charged) # "cancelled", what it cost -check = client.cancel_citecheck("12bbbf65") # like get_citecheck +check = client.cancel_citecheck("12bbbf65") # like get_citecheck print(check.status, check.credits.charged) ``` @@ -1175,6 +1175,7 @@ from lenz_io import Lenz client = Lenz() # one client for the whole app; it is safe to share across threads + async def quick_check(claim: str) -> str: out = await asyncio.to_thread(client.assess, claim=claim) return out.claims[0].verdict @@ -1191,14 +1192,17 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from lenz_io import Lenz + @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: app.state.lenz = Lenz() yield app.state.lenz.close() + app = FastAPI(lifespan=lifespan) + @app.post("/check") def check(claim: str) -> dict[str, object]: # `def`, not `async def`: FastAPI runs it in a thread v = app.state.lenz.verify_and_wait(claim) @@ -1244,8 +1248,8 @@ built. Every method takes three keyword-only request options, for that call only: ```python -client.assess(claim="...", timeout=20) # one HTTP attempt, in seconds -client.usage(max_retries=0) # no retries for this call +client.assess(claim="...", timeout=20) # one HTTP attempt, in seconds +client.usage(max_retries=0) # no retries for this call client.verify("...", extra_headers={"X-Trace-Id": trace}) # added to the request ``` diff --git a/src/lenz_io/client.py b/src/lenz_io/client.py index f5e938c..94abb14 100644 --- a/src/lenz_io/client.py +++ b/src/lenz_io/client.py @@ -408,7 +408,7 @@ class _ClientOptions: copy it was made from. ``NOT_GIVEN`` inherits from the ``httpx.Client`` in use (timeout) or the constructor (retries).""" - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN + timeout: float | httpx.Timeout | NotGiven | None = NOT_GIVEN max_retries: int | NotGiven = NOT_GIVEN headers: tuple[tuple[str, str], ...] = () @@ -1128,7 +1128,7 @@ def close(self) -> None: def with_options( self: _Client, *, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | NotGiven | None = NOT_GIVEN, max_retries: int | NotGiven = NOT_GIVEN, extra_headers: Mapping[str, str | None] | None = None, ) -> _Client: