diff --git a/.api-sync/unmodeled.json b/.api-sync/unmodeled.json index 337ed09..8e3b110 100644 --- a/.api-sync/unmodeled.json +++ b/.api-sync/unmodeled.json @@ -882,5 +882,440 @@ "reason": "Pre-existing SDK-only field on UpdateCustomerInput with no matching property on UpdateCustomerIn in the spec.", "owner": "eric@blindpay.com" } + ], + "enumPropertyOmissions": [], + "nestedShapeOmissions": [ + { + "schema": "BankAccountOut", + "path": "offramp_wallets[]", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreateCustomerIn", + "path": "owners[]", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "blindpay_bank_details", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "blindpay_bank_details.ach", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "blindpay_bank_details.beneficiary", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "blindpay_bank_details.receiving_bank", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "blindpay_bank_details.rtp", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "blindpay_bank_details.swift_receiving_bank", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "blindpay_bank_details.wire", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "tracking_complete", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "tracking_payment", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "tracking_payment.review_context", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "tracking_transaction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "tracking_transaction.pse_instruction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "tracking_transaction.ted_instruction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinOut", + "path": "tracking_transaction.transfers_instruction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreatePayinQuoteIn", + "path": "payer_rules", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreateTransferOut", + "path": "tracking_bridge_swap", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreateTransferOut", + "path": "tracking_complete", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreateTransferOut", + "path": "tracking_paymaster", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CreateTransferOut", + "path": "tracking_transaction_monitoring", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CustomerOut", + "path": "aml_hits", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CustomerOut", + "path": "fraud_warnings[]", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CustomerOut", + "path": "kyc_warnings[]", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CustomerOut", + "path": "limit", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "CustomerOut", + "path": "owners[]", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "GetCustomerLimitsOut", + "path": "limits", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "GetCustomerLimitsOut", + "path": "limits.payin", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "GetCustomerLimitsOut", + "path": "limits.payout", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "blindpay_bank_details", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "blindpay_bank_details.ach", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "blindpay_bank_details.beneficiary", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "blindpay_bank_details.receiving_bank", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "blindpay_bank_details.rtp", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "blindpay_bank_details.swift_receiving_bank", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "blindpay_bank_details.wire", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "payer_rules", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "tracking_complete", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "tracking_payment", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "tracking_payment.review_context", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "tracking_transaction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "tracking_transaction.pse_instruction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "tracking_transaction.ted_instruction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayinOut", + "path": "tracking_transaction.transfers_instruction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOnEvmOut", + "path": "tracking_complete", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOnEvmOut", + "path": "tracking_documents", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOnEvmOut", + "path": "tracking_liquidity", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOnEvmOut", + "path": "tracking_payment", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOnEvmOut", + "path": "tracking_transaction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOut", + "path": "jpm_track_data", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOut", + "path": "tracking_complete", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOut", + "path": "tracking_documents", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOut", + "path": "tracking_liquidity", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOut", + "path": "tracking_payment", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "PayoutOut", + "path": "tracking_transaction", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "QuoteOut", + "path": "contract", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "QuoteOut", + "path": "contract.abi[]", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "QuoteOut", + "path": "contract.network", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "TransferOut", + "path": "tracking_bridge_swap", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "TransferOut", + "path": "tracking_complete", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "TransferOut", + "path": "tracking_paymaster", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "TransferOut", + "path": "tracking_transaction_monitoring", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "UpdateCustomerIn", + "path": "owners[]", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "VirtualAccountOut", + "path": "blockchain_wallet", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "VirtualAccountOut", + "path": "us", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "VirtualAccountOut", + "path": "us.ach", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "VirtualAccountOut", + "path": "us.beneficiary", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "VirtualAccountOut", + "path": "us.receiving_bank", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "VirtualAccountOut", + "path": "us.rtp", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "VirtualAccountOut", + "path": "us.swift_intermediary_bank", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "VirtualAccountOut", + "path": "us.swift_receiving_bank", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + }, + { + "schema": "VirtualAccountOut", + "path": "us.wire", + "reason": "Pre-existing inline nested shape from before the api-sync-hardening pass introduced nested-object coverage checking; recorded intentionally (not modeled) so newly introduced unmapped nested shapes fail loudly instead of silently. Needs a human pass to either model or drop.", + "owner": "eric@blindpay.com" + } ] } diff --git a/.github/workflows/api-sync.yml b/.github/workflows/api-sync.yml index 0fda453..79f3155 100644 --- a/.github/workflows/api-sync.yml +++ b/.github/workflows/api-sync.yml @@ -20,11 +20,41 @@ jobs: with: bun-version: 1.2.18 - - name: Fetch the delivered spec + # Fails loudly rather than syncing against a wrong, truncated, or already-superseded spec: + # the delivery branch must carry both spec-current.json and a delivery.json manifest whose + # spec_sha256 actually hashes to spec-current.json's bytes, and (when the dispatch payload + # carries its own spec_sha256) the manifest must agree with what triggered this run. + - name: Fetch and verify the delivered spec manifest + env: + DISPATCH_SHA256: ${{ github.event.client_payload.spec_sha256 }} run: | + set -e git fetch origin api-sync-data mkdir -p .api-sync - git show origin/api-sync-data:.api-sync/spec-current.json > .api-sync/spec-current.json + + if ! git show origin/api-sync-data:.api-sync/spec-current.json > .api-sync/spec-current.json; then + echo "::error::.api-sync/spec-current.json is missing on the api-sync-data branch." + exit 1 + fi + if ! git show origin/api-sync-data:.api-sync/delivery.json > .api-sync/delivery.json; then + echo "::error::.api-sync/delivery.json is missing on the api-sync-data branch." + exit 1 + fi + + MANIFEST_SHA256=$(jq -r '.spec_sha256' .api-sync/delivery.json) + ACTUAL_SHA256=$(sha256sum .api-sync/spec-current.json | cut -d' ' -f1) + if [ "$MANIFEST_SHA256" != "$ACTUAL_SHA256" ]; then + echo "::error::spec-current.json sha256 ($ACTUAL_SHA256) does not match delivery.json's spec_sha256 ($MANIFEST_SHA256)." + exit 1 + fi + + if [ -n "$DISPATCH_SHA256" ] && [ "$DISPATCH_SHA256" != "$MANIFEST_SHA256" ]; then + echo "::error::stale delivery: the dispatch payload's spec_sha256 ($DISPATCH_SHA256) differs from delivery.json's ($MANIFEST_SHA256)." + exit 1 + fi + + echo "Verified spec-current.json (sha256 $ACTUAL_SHA256) against delivery.json:" + jq -r '" source_repository: \(.source_repository)\n source_sha: \(.source_sha)\n generated_at: \(.generated_at)"' .api-sync/delivery.json - name: Check for existing api-sync PR id: check-pr diff --git a/scripts/api-sync/enum-coverage.test.ts b/scripts/api-sync/enum-coverage.test.ts new file mode 100644 index 0000000..1551316 --- /dev/null +++ b/scripts/api-sync/enum-coverage.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, it } from "vitest"; +import { findEnumConstraints, findUnmappedEnumProperties } from "./enum-coverage"; +import type { Spec } from "./spec"; +import type { SpecMap, UnmodeledFile } from "./types"; + +function emptyMap(overrides: Partial = {}): SpecMap { + return { enums: [], types: [], ignore: { schemas: [] }, ...overrides }; +} + +function emptyUnmodeled(overrides: Partial = {}): UnmodeledFile { + return { + properties: [], + knownDivergences: [], + enumPropertyOmissions: [], + nestedShapeOmissions: [], + ...overrides, + }; +} + +describe("findEnumConstraints", () => { + it("finds direct, items, anyOf, and oneOf enum-constrained properties", () => { + const spec: Spec = { + paths: { "/x": { get: { responses: { "200": {} } } } }, + components: { + schemas: { + S: { + type: "object", + properties: { + direct: { type: "string", enum: ["a", "b"] }, + items: { type: "array", items: { type: "string", enum: ["a"] } }, + wrappedAny: { + anyOf: [{ type: "string", enum: ["a"] }, { type: "null" }], + }, + wrappedOne: { + oneOf: [{ type: "string", enum: ["a"] }, { type: "null" }], + }, + plain: { type: "string" }, + }, + }, + }, + }, + }; + // S must be reachable: seed it via a $ref from paths. + spec.paths["/x"].get.responses["200"] = { + content: { "application/json": { schema: { $ref: "#/components/schemas/S" } } }, + }; + const kinds = findEnumConstraints(spec, emptyMap()) + .map((c) => `${c.property}:${c.kind}`) + .sort(); + expect(kinds).toEqual([ + "direct:direct", + "items:items", + "wrappedAny:anyOf", + "wrappedOne:oneOf", + ]); + }); + + it("skips schemas listed in map.ignore.schemas", () => { + const spec: Spec = { + paths: { + "/x": { + get: { + responses: { + "200": { + content: { + "application/json": { + schema: { $ref: "#/components/schemas/S" }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + S: { type: "object", properties: { f: { type: "string", enum: ["a"] } } }, + }, + }, + }; + const map = emptyMap({ ignore: { schemas: [{ name: "S", reason: "n/a" }] } }); + expect(findEnumConstraints(spec, map)).toEqual([]); + }); +}); + +describe("findUnmappedEnumProperties", () => { + const baseSpec: Spec = { + paths: { + "/x": { + get: { + responses: { + "200": { + content: { + "application/json": { schema: { $ref: "#/components/schemas/S" } }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + S: { + type: "object", + properties: { + direct: { type: "string", enum: ["a", "b"] }, + wrapped: { anyOf: [{ type: "string", enum: ["a"] }, { type: "null" }] }, + }, + }, + }, + }, + }; + + it("a direct/items enum on a schema covered by a map.types entry is not a gap, even without a dedicated enums[] entry", () => { + const map = emptyMap({ types: [{ spec: { schema: "S" }, sdk: [] }] }); + const gaps = findUnmappedEnumProperties(baseSpec, map, emptyUnmodeled()); + expect(gaps.map((g) => g.property)).toEqual(["wrapped"]); + }); + + it("a direct enum with a dedicated map.enums entry is covered even without a map.types entry", () => { + const map = emptyMap({ + enums: [{ spec: { schema: "S", property: "direct" }, sdk: [] }], + }); + const gaps = findUnmappedEnumProperties(baseSpec, map, emptyUnmodeled()); + expect(gaps.map((g) => g.property).sort()).toEqual(["wrapped"]); + }); + + it("anyOf/oneOf-wrapped enums are never covered by the ordinary field-type comparison", () => { + const map = emptyMap({ types: [{ spec: { schema: "S" }, sdk: [] }] }); + const gaps = findUnmappedEnumProperties(baseSpec, map, emptyUnmodeled()); + expect(gaps).toEqual([{ schema: "S", property: "wrapped", kind: "anyOf" }]); + }); + + it("a recorded omission suppresses the gap", () => { + const map = emptyMap({ types: [{ spec: { schema: "S" }, sdk: [] }] }); + const unmodeled = emptyUnmodeled({ + enumPropertyOmissions: [ + { schema: "S", property: "wrapped", reason: "test", owner: "eric@blindpay.com" }, + ], + }); + expect(findUnmappedEnumProperties(baseSpec, map, unmodeled)).toEqual([]); + }); +}); diff --git a/scripts/api-sync/enum-coverage.ts b/scripts/api-sync/enum-coverage.ts new file mode 100644 index 0000000..23c2d58 --- /dev/null +++ b/scripts/api-sync/enum-coverage.ts @@ -0,0 +1,93 @@ +import { reachableSchemas, resolveProperties, type Spec } from "./spec"; +import { isPathLocator, type SpecMap, type UnmodeledFile } from "./types"; + +export type EnumConstraintKind = "direct" | "items" | "anyOf" | "oneOf"; + +export type EnumConstraint = { + schema: string; + property: string; + kind: EnumConstraintKind; +}; + +// biome-ignore lint/suspicious/noExplicitAny: raw OpenAPI schema node +function enumKind(node: any): EnumConstraintKind | null { + if (!node || typeof node !== "object") return null; + if (Array.isArray(node.enum)) return "direct"; + const t = node.type; + const isArray = Array.isArray(t) ? t.includes("array") : t === "array"; + // biome-ignore lint/suspicious/noExplicitAny: raw OpenAPI schema node + if (isArray && node.items && Array.isArray((node.items as any).enum)) return "items"; + // biome-ignore lint/suspicious/noExplicitAny: raw OpenAPI schema node + if (Array.isArray(node.anyOf) && node.anyOf.some((m: any) => Array.isArray(m?.enum))) + return "anyOf"; + // biome-ignore lint/suspicious/noExplicitAny: raw OpenAPI schema node + if (Array.isArray(node.oneOf) && node.oneOf.some((m: any) => Array.isArray(m?.enum))) + return "oneOf"; + return null; +} + +/** Every enum-constrained top-level property on every in-scope (reachable, non-ignored) schema. */ +export function findEnumConstraints(spec: Spec, map: SpecMap): EnumConstraint[] { + const reachable = reachableSchemas(spec); + const ignored = new Set(map.ignore.schemas.map((e) => e.name)); + const out: EnumConstraint[] = []; + for (const schemaName of reachable) { + if (ignored.has(schemaName)) continue; + const node = spec.components.schemas[schemaName]; + const props = node?.properties ?? {}; + for (const [property, propNode] of Object.entries(props)) { + const kind = enumKind(propNode); + if (kind) out.push({ schema: schemaName, property, kind }); + } + } + return out; +} + +function isMappedEnumSymbol(map: SpecMap, c: EnumConstraint): boolean { + return map.enums.some((e) => { + if (isPathLocator(e.spec)) return false; + if (e.spec.schema !== c.schema || e.spec.property !== c.property) return false; + return c.kind === "items" ? e.spec.itemsEnum === true : !e.spec.itemsEnum; + }); +} + +/** + * "direct" and "items" enums on a schema.property already covered by a map.types entry are not + * invisible: reconcile.ts's per-field type comparison (specTypeSignature) reads node.enum / + * node.items.enum and flags any drift as a needs-human failure or a recorded known-divergence, + * even without a dedicated map.enums entry. "anyOf"/"oneOf"-wrapped enums are the real blind + * spot: specTypeSignature does not look inside anyOf/oneOf, so a property shaped that way is + * genuinely unchecked by any existing mechanism. + */ +function isCoveredByFieldTypeCheck(spec: Spec, map: SpecMap, c: EnumConstraint): boolean { + if (c.kind !== "direct" && c.kind !== "items") return false; + for (const entry of map.types) { + if (isPathLocator(entry.spec) || entry.spec.schema !== c.schema) continue; + try { + if (resolveProperties(spec, entry.spec).has(c.property)) return true; + } catch { + // locator doesn't resolve; reconcile.ts already reports this separately + } + } + return false; +} + +function isRecordedOmission(unmodeled: UnmodeledFile, c: EnumConstraint): boolean { + return unmodeled.enumPropertyOmissions.some( + (e) => e.schema === c.schema && e.property === c.property + ); +} + +/** Enum-constrained properties invisible to both the enum-insert patcher and the field-type audit. */ +export function findUnmappedEnumProperties( + spec: Spec, + map: SpecMap, + unmodeled: UnmodeledFile +): EnumConstraint[] { + return findEnumConstraints(spec, map).filter( + (c) => + !isMappedEnumSymbol(map, c) && + !isCoveredByFieldTypeCheck(spec, map, c) && + !isRecordedOmission(unmodeled, c) + ); +} diff --git a/scripts/api-sync/index.ts b/scripts/api-sync/index.ts index 41d73c3..a05a961 100644 Binary files a/scripts/api-sync/index.ts and b/scripts/api-sync/index.ts differ diff --git a/scripts/api-sync/map.test.ts b/scripts/api-sync/map.test.ts index 1568194..1513a53 100644 --- a/scripts/api-sync/map.test.ts +++ b/scripts/api-sync/map.test.ts @@ -1,6 +1,8 @@ import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; +import { findUnmappedEnumProperties } from "./enum-coverage"; import { isKnownEnumDivergence, isUnmodeledProperty, loadSpecMap, loadUnmodeled } from "./map"; +import { findUnmappedNestedShapes } from "./nested-coverage"; import { findDeclarationSpan, readSource } from "./sdk-scan"; import { loadSpec, @@ -64,6 +66,16 @@ describe("spec-map.json validity", () => { const uncovered = [...reachable].filter((n) => !mappedNames.has(n) && !ignoredNames.has(n)); expect(uncovered).toEqual([]); }); + + it("every enum-constrained property resolves to a mapped enum symbol, the ordinary field-type audit, or a recorded omission", () => { + const gaps = findUnmappedEnumProperties(spec, map, unmodeled); + expect(gaps).toEqual([]); + }); + + it("every inline nested object/array-item shape has a recorded omission (the map format has no nested entries yet)", () => { + const gaps = findUnmappedNestedShapes(spec, map, unmodeled); + expect(gaps).toEqual([]); + }); }); describe("unmodeled.json validity", () => { diff --git a/scripts/api-sync/map.ts b/scripts/api-sync/map.ts index 6944ed3..e15f1a3 100644 --- a/scripts/api-sync/map.ts +++ b/scripts/api-sync/map.ts @@ -16,8 +16,15 @@ export function loadSpecMap(path: string): SpecMap { export function loadUnmodeled(path: string): UnmodeledFile { const raw = JSON.parse(readFileSync(path, "utf8")); - if (!Array.isArray(raw.properties) || !Array.isArray(raw.knownDivergences)) { - throw new Error(`${path}: expected {properties: [], knownDivergences: []}`); + if ( + !Array.isArray(raw.properties) || + !Array.isArray(raw.knownDivergences) || + !Array.isArray(raw.enumPropertyOmissions) || + !Array.isArray(raw.nestedShapeOmissions) + ) { + throw new Error( + `${path}: expected {properties: [], knownDivergences: [], enumPropertyOmissions: [], nestedShapeOmissions: []}` + ); } return raw; } diff --git a/scripts/api-sync/nested-coverage.test.ts b/scripts/api-sync/nested-coverage.test.ts new file mode 100644 index 0000000..f3228de --- /dev/null +++ b/scripts/api-sync/nested-coverage.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from "vitest"; +import { findNestedShapes, findUnmappedNestedShapes } from "./nested-coverage"; +import type { Spec } from "./spec"; +import type { SpecMap, UnmodeledFile } from "./types"; + +function emptyMap(overrides: Partial = {}): SpecMap { + return { enums: [], types: [], ignore: { schemas: [] }, ...overrides }; +} + +function emptyUnmodeled(overrides: Partial = {}): UnmodeledFile { + return { + properties: [], + knownDivergences: [], + enumPropertyOmissions: [], + nestedShapeOmissions: [], + ...overrides, + }; +} + +function specWithSchema(schema: Record): Spec { + return { + paths: { + "/x": { + get: { + responses: { + "200": { + content: { + "application/json": { schema: { $ref: "#/components/schemas/S" } }, + }, + }, + }, + }, + }, + }, + components: { schemas: { S: schema } }, + }; +} + +describe("findNestedShapes", () => { + it("finds an inline nested object property", () => { + const spec = specWithSchema({ + type: "object", + properties: { nested: { type: "object", properties: { a: { type: "string" } } } }, + }); + expect(findNestedShapes(spec, emptyMap())).toEqual([{ schema: "S", path: "nested" }]); + }); + + it("finds an inline object shape under an array property and marks the array hop", () => { + const spec = specWithSchema({ + type: "object", + properties: { + items: { + type: "array", + items: { type: "object", properties: { a: { type: "string" } } }, + }, + }, + }); + expect(findNestedShapes(spec, emptyMap())).toEqual([{ schema: "S", path: "items[]" }]); + }); + + it("does not descend into a $ref'd schema (that's a reusable named schema, not an inline shape)", () => { + const spec = specWithSchema({ + type: "object", + properties: { other: { $ref: "#/components/schemas/Other" } }, + }); + expect(findNestedShapes(spec, emptyMap())).toEqual([]); + }); + + it("recurses into nested-within-nested shapes", () => { + const spec = specWithSchema({ + type: "object", + properties: { + outer: { + type: "object", + properties: { + inner: { type: "object", properties: { a: { type: "string" } } }, + }, + }, + }, + }); + const paths = findNestedShapes(spec, emptyMap()) + .map((s) => s.path) + .sort(); + expect(paths).toEqual(["outer", "outer.inner"]); + }); + + it("skips schemas listed in map.ignore.schemas", () => { + const spec = specWithSchema({ + type: "object", + properties: { nested: { type: "object", properties: { a: { type: "string" } } } }, + }); + const map = emptyMap({ ignore: { schemas: [{ name: "S", reason: "n/a" }] } }); + expect(findNestedShapes(spec, map)).toEqual([]); + }); +}); + +describe("findUnmappedNestedShapes", () => { + it("a recorded omission suppresses the gap", () => { + const spec = specWithSchema({ + type: "object", + properties: { nested: { type: "object", properties: { a: { type: "string" } } } }, + }); + const unmodeled = emptyUnmodeled({ + nestedShapeOmissions: [ + { schema: "S", path: "nested", reason: "test", owner: "eric@blindpay.com" }, + ], + }); + expect(findUnmappedNestedShapes(spec, emptyMap(), unmodeled)).toEqual([]); + }); + + it("an unrecorded nested shape is a gap", () => { + const spec = specWithSchema({ + type: "object", + properties: { nested: { type: "object", properties: { a: { type: "string" } } } }, + }); + expect(findUnmappedNestedShapes(spec, emptyMap(), emptyUnmodeled())).toEqual([ + { schema: "S", path: "nested" }, + ]); + }); +}); diff --git a/scripts/api-sync/nested-coverage.ts b/scripts/api-sync/nested-coverage.ts new file mode 100644 index 0000000..34117c5 --- /dev/null +++ b/scripts/api-sync/nested-coverage.ts @@ -0,0 +1,69 @@ +import { reachableSchemas, type Spec } from "./spec"; +import type { SpecMap, UnmodeledFile } from "./types"; + +export type NestedShape = { + schema: string; + /** Dot-path from the schema root, e.g. "tracking_transaction.pse_instruction". "[]" marks an array-item hop. */ + path: string; +}; + +// biome-ignore lint/suspicious/noExplicitAny: raw OpenAPI schema node +function isObjectNode(n: any): boolean { + if (!n || typeof n !== "object" || n.$ref) return false; + const t = n.type; + const types: string[] = Array.isArray(t) ? t : t ? [t] : []; + return types.includes("object") || (!!n.properties && types.length === 0); +} + +// biome-ignore lint/suspicious/noExplicitAny: raw OpenAPI schema node +function isArrayNode(n: any): boolean { + const t = n?.type; + const types: string[] = Array.isArray(t) ? t : t ? [t] : []; + return types.includes("array"); +} + +// biome-ignore lint/suspicious/noExplicitAny: raw OpenAPI schema node +function walk(schemaName: string, node: any, path: string, out: NestedShape[]): void { + if (!node?.properties) return; + for (const [property, propNode] of Object.entries(node.properties) as [string, unknown][]) { + const subpath = path ? `${path}.${property}` : property; + // biome-ignore lint/suspicious/noExplicitAny: raw OpenAPI schema node + let target = propNode as any; + let arrayHop = false; + if (isArrayNode(target) && target.items) { + target = target.items; + arrayHop = true; + } + if (target?.$ref) continue; // reusable named schema, not an inline shape + if (isObjectNode(target)) { + const fullPath = arrayHop ? `${subpath}[]` : subpath; + out.push({ schema: schemaName, path: fullPath }); + walk(schemaName, target, fullPath, out); + } + } +} + +/** Every inline nested object/array-item-object shape under every in-scope (reachable, non-ignored) schema. */ +export function findNestedShapes(spec: Spec, map: SpecMap): NestedShape[] { + const reachable = reachableSchemas(spec); + const ignored = new Set(map.ignore.schemas.map((e) => e.name)); + const out: NestedShape[] = []; + for (const schemaName of reachable) { + if (ignored.has(schemaName)) continue; + walk(schemaName, spec.components.schemas[schemaName], "", out); + } + return out; +} + +function isRecordedOmission(unmodeled: UnmodeledFile, s: NestedShape): boolean { + return unmodeled.nestedShapeOmissions.some((e) => e.schema === s.schema && e.path === s.path); +} + +/** Nested shapes with neither a map entry (none exist yet; the map format is flat) nor a recorded omission. */ +export function findUnmappedNestedShapes( + spec: Spec, + map: SpecMap, + unmodeled: UnmodeledFile +): NestedShape[] { + return findNestedShapes(spec, map).filter((s) => !isRecordedOmission(unmodeled, s)); +} diff --git a/scripts/api-sync/types.ts b/scripts/api-sync/types.ts index d2a5e24..7cf0758 100644 --- a/scripts/api-sync/types.ts +++ b/scripts/api-sync/types.ts @@ -87,9 +87,28 @@ export type KnownDivergenceEntry = | KnownDivergencePropertyEntry | KnownDivergenceTypeMismatchEntry; +export type EnumPropertyOmissionEntry = { + schema: string; + property: string; + reason: string; + owner: string; +}; + +export type NestedShapeOmissionEntry = { + schema: string; + path: string; + reason: string; + owner: string; +}; + export type UnmodeledFile = { properties: UnmodeledPropertyEntry[]; knownDivergences: KnownDivergenceEntry[]; + /** Enum-constrained properties intentionally not (yet) covered by spec-map.json's enums[] + * or by the ordinary field-type comparison; see enum-coverage.ts. */ + enumPropertyOmissions: EnumPropertyOmissionEntry[]; + /** Inline nested object/array-item shapes intentionally not (yet) modeled; see nested-coverage.ts. */ + nestedShapeOmissions: NestedShapeOmissionEntry[]; }; export type ApplicableChange =