"""Unaccepted field-evidence format; no report projection or business policy. Paths identify observations, not interchangeable values or fallback priorities. The independent extractor and verifier share only this declarative contract and the exact scalar encoding. All arrays retain their original order and indexes. """ from __future__ import annotations from decimal import Decimal import hashlib import json VERSION = "arr-source-field-evidence/v1" MAX_FACTS_BYTES = 128 * 1024 * 1024 FLAGS = {"source_mapping_verified": False, "report_equivalence_verified": False, "finance_ready": False} # Each tuple is (source, variant, relative path). No predicate, joining, # formatting, defaulting or preferred alternative is implied by this list. FIELDS = { # Preserve the original evidence contract. Typed BlockCode values observed # later are already retained under BLOCK_ID + BLOCK_ID_TYPE context below; # preparation/v3 selects them from the verified raw search/detail documents. "BLOCK_CODE": (), "ADULTS": (("detail", "stay", "roomStay/guestCounts/adults"), ("detail", "rate_segments", "roomStay/roomRates/*/guestCounts/adults")), "CHILDREN": (("detail", "stay", "roomStay/guestCounts/children"), ("detail", "rate_segments", "roomStay/roomRates/*/guestCounts/children")), "COMPANY_NAME": (("detail", "reservation_profiles", "reservationProfiles/reservationProfile/*/profile/company/companyName"), ("detail", "rate_profiles", "roomStay/roomRates/*/stayProfiles/*/profile/company/companyName")), # These are typed ID candidates; ID_TYPE context is mandatory. This stage # deliberately does not pick the first ID or assume each ID is Confirmation. "CONFIRMATION_NO": (("detail", "typed_id_candidates", "reservationIdList/*/id"),), "DISP_ROOM_NO": (("detail", "current_room", "roomStay/currentRoomInfo/roomId"), ("detail", "rate_segments", "roomStay/roomRates/*/roomId")), "EFFECTIVE_RATE_AMOUNT": (("rate", "raw_day_rate", "detail/totalRateAmount"), ("assessment", "validated_day_rate", "effective_rate")), "FULL_NAME": (("search", "display_name", "reservationGuest/fullName"),), "RES_COMMENT": (("detail", "all_returned_comments", "comments/*/comment/text/value"),), "TRACE_TEXT": (("detail", "all_returned_traces", "traces/*/traceText"),), "NO_OF_ROOMS": (("detail", "rate_segments", "roomStay/roomRates/*/numberOfUnits"),), "PRODUCTS": (("detail", "package_codes_only", "reservationPackages/*/packageCode"),), "RATE_CODE": (("detail", "rate_segments", "roomStay/roomRates/*/ratePlanCode"),), "ROOM_CATEGORY_LABEL": (("detail", "room_type_codes_only", "roomStay/roomRates/*/roomType"),), "ARRIVAL": (("detail", "stay", "roomStay/arrivalDate"),), "DEPARTURE": (("detail", "stay", "roomStay/departureDate"),), } CONTEXT_PATHS = { "RESERVATION_STATUS": (("detail", "status", "reservationStatus"),), "ID_TYPE": (("detail", "typed_ids", "reservationIdList/*/type"),), "RATE_START": (("detail", "segments", "roomStay/roomRates/*/start"),), "RATE_END": (("detail", "segments", "roomStay/roomRates/*/end"),), "PROFILE_ROLE": (("detail", "reservation", "reservationProfiles/reservationProfile/*/reservationProfileType"), ("detail", "rate_segments", "roomStay/roomRates/*/stayProfiles/*/reservationProfileType")), "GUEST_PRIMARY": (("detail", "guests", "reservationGuests/*/primary"),), "NAME_TYPE": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/nameType"),), "SURNAME": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/surname"),), "GIVEN_NAME": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/givenName"),), "MIDDLE_NAME": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/middleName"),), "NAME_TITLE": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/nameTitle"),), "COMMENT_TYPE": (("detail", "comments", "comments/*/comment/type"),), "COMMENT_LOCATION": (("detail", "comments", "comments/*/comment/notificationLocation"),), "COMMENT_INTERNAL": (("detail", "comments", "comments/*/comment/internal"),), "COMMENT_CONFIDENTIAL": (("detail", "comments", "comments/*/comment/confidential"),), "TRACE_RESOLVED_ON": (("detail", "traces", "traces/*/resolveInfo/resolvedOn"),), "TRACE_RESOLVED_BY": (("detail", "traces", "traces/*/resolveInfo/resolvedBy"),), "BLOCK_ID": (("detail", "segments", "roomStay/roomRates/*/reservationBlock/blockIdList/*/id"),), "BLOCK_ID_TYPE": (("detail", "segments", "roomStay/roomRates/*/reservationBlock/blockIdList/*/type"),), "DAY_RATE_CURRENCY": (("rate", "raw", "detail/revenue/currencyCode"), ("assessment", "validated", "currency")), "DETAIL_CURRENCY": (("detail", "rates", "roomStay/roomRates/*/rates/rate/*/base/currencyCode"),), "RATE_SUPPRESSED": (("rate", "raw", "detail/rateSuppressed"),), "RATE_ISSUE": (("assessment", "validation", "error"),), } BLOCKERS = ("report_inclusion", "report_order_and_row_granularity", "company_role_and_display", "name_display", "historical_room", "note_and_trace_scope_order", "rate_interval_semantics", "optional_report_displays", "same_source_acceptance") def canonical(value) -> bytes: return (json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"), allow_nan=False) + "\n").encode() CONTRACT_SHA256 = hashlib.sha256(canonical({"version": VERSION, "fields": FIELDS, "context": CONTEXT_PATHS, "flags": FLAGS, "blockers": BLOCKERS})).hexdigest() # v1 stays byte-for-byte compatible with evidence from three-operation captures. # v2 adds observations from the fourth operation; no report mapping is selected. NAMED_VERSION = "arr-source-field-evidence/v2" NAMED_FIELDS = {**FIELDS, "FULL_NAME": (*FIELDS["FULL_NAME"], ("profile", "raw_summary_name", "profile/formerName/fullName"), ("assessment", "validated_primary_name", "profile/full_name"))} NAMED_CONTEXT_PATHS = {**CONTEXT_PATHS, "GUEST_PROFILE_ID": (("detail", "guests", "reservationGuests/*/profileInfo/profileIdList/*/id"),), "GUEST_PROFILE_ID_TYPE": (("detail", "guests", "reservationGuests/*/profileInfo/profileIdList/*/type"),), "NAME_PREFIX": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/namePrefix"),), "SUMMARY_PROFILE_ID": (("profile", "identity", "profileIdList/*/id"),), "SUMMARY_PROFILE_ID_TYPE": (("profile", "identity", "profileIdList/*/type"),), "SUMMARY_NAME_TYPE": (("profile", "name", "profile/formerName/nameType"),), "SUMMARY_SURNAME": (("profile", "name", "profile/formerName/name"),), "SUMMARY_GIVEN_NAME": (("profile", "name", "profile/formerName/givenName"),), "SUMMARY_MIDDLE_NAME": (("profile", "name", "profile/formerName/middleName"),), "SUMMARY_NAME_TITLE": (("profile", "name", "profile/formerName/nameTitle"),), "SUMMARY_NAME_PREFIX": (("profile", "name", "profile/formerName/namePrefix"),), "NAME_ISSUE": (("assessment", "validation", "profile/error"),)} NAMED_BLOCKERS = tuple("note_scope_order" if item == "note_and_trace_scope_order" else item for item in BLOCKERS) NAMED_CONTRACT_SHA256 = hashlib.sha256(canonical({"version": NAMED_VERSION, "fields": NAMED_FIELDS, "context": NAMED_CONTEXT_PATHS, "flags": FLAGS, "blockers": NAMED_BLOCKERS})).hexdigest() def definitions(named=False): """Select declarations only; extraction and joining remain independent.""" return ((NAMED_VERSION, NAMED_CONTRACT_SHA256, NAMED_FIELDS, NAMED_CONTEXT_PATHS, NAMED_BLOCKERS) if named else (VERSION, CONTRACT_SHA256, FIELDS, CONTEXT_PATHS, BLOCKERS)) def kind(value) -> str: return {str: "string", int: "integer", Decimal: "decimal", bool: "boolean", dict: "object", list: "array", type(None): "null"}.get(type(value), "unsupported") def atom(value) -> dict: """Lossless scalar encoding, not business validation; booleans stay booleans.""" tag = kind(value) if tag == "null": return {"state": "null", "kind": tag} if tag in {"object", "array", "unsupported"}: return {"state": "non_scalar", "kind": tag} return {"state": "explicit_blank" if type(value) is str and not value.strip() else "present", "kind": tag, "value": str(value) if type(value) in {int, Decimal} else value}