Skip to content

feat: deterministic api-sync patcher (Phase A) - #68

Merged
ericviana merged 3 commits into
mainfrom
eric/deterministic-api-sync
Aug 4, 2026
Merged

ericviana merged 3 commits into
mainfrom
eric/deterministic-api-sync

Conversation

@ericviana

Copy link
Copy Markdown
Member

What this is

Phase A of removing the AI (Claude + CLAUDE_CODE_OAUTH_TOKEN) from this repo's api-sync pipeline, replacing it with a deterministic, state-based patcher, per the shared design doc.

scripts/api-sync/ reconciles this SDK's declared TypeScript surface against the current delivered spec state (not an event diff against the previous snapshot, since event diffing is exactly the mechanism that let refund_wallet_address and the portage banking partner sit unmodeled for who knows how long: neither ever showed up as a "diff" once the spec settled). For every schema/enum in the curated .api-sync/spec-map.json:

  • a missing optional property or enum member becomes an applicable change (auto-inserted, field?: T, scoped-anchor text edit, never a reformat);
  • a removal, a required-ness or type change, a new operation/schema, or a mapped anchor that no longer resolves is needs-human (hard fail, precise message, nothing silently dropped);
  • anything already recorded in .api-sync/unmodeled.json (property gaps) or as a knownDivergences entry (enum-value mismatches, enum members, or type mismatches) is a known divergence: skipped, but greppable, with a reason and an owner.

Determinism: sorted output, no timestamps, byte-verbatim snapshot refresh (copyFileSync, never JSON.parse+stringify), surgical text insertion only (never reformats a touched file).

Applied this run

Exactly the two changes verified safe against the spec and the API source, nothing more:

  • QuoteIn.refund_wallet_address -> CreateQuoteInput.refund_wallet_address?: string | null (quotes/index.ts)
  • portage added to the banking_partner enum -> both physical BankingPartner declarations (types/index.d.ts, which is dead/unused code, and virtual-accounts/index.ts, which is the one actually referenced)

Version bump: minor, 5.0.1 -> 5.1.0 (an enum member was added).

.api-sync/spec-map.json

Bootstrapped by scoring every exported SDK type's field-set against every spec schema's property-set, then every one of the 39 enum entries and 85 type entries was verified by reading the source. Covers the two discriminator fan-outs called out in the brief:

  • CreateBankAccountIn -> the 11 rail create-input types (not 10, CreateTedInput was missing from an earlier draft of the map; corrected. type is a factory-injected discriminator on all 11, never carried on the input type by design).
  • CreateCustomerIn/CustomerOut -> the 3 KYC/KYB variants (type/kyc_type likewise factory-injected).

ignore.schemas (28 entries): the Rfi family (+2 orphan *Body remnants), UploadAnalyzeIn/Out, LedgerOperation (orphan, zero $ref anywhere), and the 17 webhook-payload-only schemas this SDK never models (it only verifies signatures; consumers parse the JSON body themselves). Every one of the 84 reachable schemas (transitive $ref from paths plus the top-level webhooks object) is either mapped or listed here, verified programmatically and by a vitest map-validity test that runs against the real committed files.

.api-sync/unmodeled.json

Two sections: properties (80 entries, spec properties absent from a mapped SDK type) and knownDivergences (43 entries: enum, property, or type-mismatch gaps), each with a specific reason and owner: eric@blindpay.com.

The three items called out explicitly in the brief:

  • tracking_payment: I have to correct the brief here after reading the code. This SDK's PayoutTrackingPayment already models 12 of 20 payout fields (the 6 recipient_* fields, plus provider_name/provider_transaction_id/provider_status/estimated_time_of_arrival/completed_at), not 6. The genuinely missing 8 are coelsa_id, end_to_end_id, provider_clearing_system, provider_error_reason, provider_imad, provider_integration, provider_reference, provider_uetr, recorded, not applied, same reasoning (needs one reviewed cross-SDK PR). Payin-side tracking_payment also turned out to have its own 3-field gap (review_source, review_context, approved_risk_sources) not mentioned in the brief; recorded too. PayoutOut.tracking_documents (an entire new tracking step) and several tracking_transaction/tracking_complete fields are also missing on both sides, all recorded.
  • kyc_status: 7 of 8 modeled, missing approved_rfi, recorded as a known divergence (reached only through the unmodeled RFI resource).
  • bank account type: confirmed correct, untouched ("checking" | "saving" matches packages/api-contract/src/bank-account.schema.ts exactly).

Also recorded: Error's envelope only ever exposes message (not the newer code/description/errors/success/trace_id); a real type bug, Payin.billing_fee_amount is declared string | null in the SDK but number in the spec; several bare-string enum regressions (BankAccountOut.business_industry/spei_protocol/status/swift_payment_code, AvailableRails.country, CustomerOut.source_of_funds_doc_type, PayoutOut.ach_cop_document_type); Rail missing sepa even though SEPA bank-account creation already works; BankDetailKey missing 24 of 73 display-label values; EstimatedTimeOfArrival missing 5_30_min; and two dead SDK-only types with no live spec operation behind them, CreatePayoutInput (already known, see below) and CreatePayinInput (same shape of problem, field name does not even match the spec).

Known follow-up (documented, not acted on)

blindpay-v2 PR #1925 prunes 49 unreachable schemas from the public spec (my own reachability computation independently found exactly 49 orphans, a good cross-check). Against that pruned spec, CreatePayoutInput.reference (payouts/index.ts:112, dead code, no live operation posts a body shaped like it) stops resolving under contract-check.mjs's weak "exists anywhere in the spec" check, because today it coincidentally matches an internal billing-invoice schema's reference property. Not fixing it now: this repo's contract-check hard-fails on an allow-list entry that matches nothing, and the current snapshot still contains those orphans. scripts/contract-check.mjs itself is unchanged and will surface this the moment the snapshot is refreshed to the pruned spec, with its existing file:line message.

CI

  • .github/workflows/api-sync-check.yaml (new, wired into main.yaml next to contract-check): sync:check (state plus map validity) against the committed snapshot, a determinism proof (apply into two git worktrees, diff byte-identical), and a non-blocking coverage plus type-audit report.
  • bun run audit:types (--audit-types, new): full state-based type comparison including nullability, which sync:check deliberately does not enforce. This codebase's response types do not mirror JSON nullability into | null as a matter of pervasive pre-existing style (454 instances), not oversight, and retrofitting that is separate, human-reviewed, Phase C work. 1 genuine type-kind-mismatch finding (the billing_fee_amount bug above); the rest is nullability plus a handful of enum-narrowed-or-dropped cases already tracked in unmodeled.json.
  • .github/workflows/api-sync.yml: keeps the repository_dispatch: [api-sync] trigger, drops the claude-code-action step and all CLAUDE_CODE_OAUTH_TOKEN references. Fetches .api-sync/spec-current.json from origin/api-sync-data, runs the patcher, exits quietly with nothing to apply, fails loudly with the needsHuman list otherwise, else commits code plus refreshed snapshot plus version bump to api-sync, opens or updates the PR (feat: ... (vX.Y.Z) for minor, fix: for patch, labeled api-sync), and enables auto-merge.
  • api-sync-merged.yml untouched, still works (operates on api-sync-data bookkeeping, independent of this change).

Proof (local gates, all green)

$ bun install
Checked 247 installs across 293 packages (no changes)

$ bun run lint:check
Checked 73 files in 36ms. No fixes applied.

$ bun run check-types
$ tsc --noEmit
(no output, exit 0)

$ bun run test
 Test Files  27 passed (27)
      Tests  163 passed (163)
(97 pre-existing + 66 new, across apply/sdk-scan/type-check/reconcile/diff/version/map/snapshot)

$ node scripts/contract-check.mjs
[contract-check] OK, 1429 declared field(s) checked, 23 webhook event(s) checked, 27 allow-list entries in use.

$ bun run sync:check
[api-sync] sync:check OK, no pending drift (133 known divergence(s) recorded and skipped).

Determinism proof (two independent git worktrees from the same commit, --apply --spec .api-sync/spec-snapshot.json):

$ diff -rq /tmp/api-sync-det-a /tmp/api-sync-det-b --exclude=node_modules --exclude=.git --exclude=bun.lock
(no output, byte-identical)

Also re-verified from a clean v5.0.1 checkout applying the real pending drift (refund_wallet_address plus portage) into two independent clones: byte-identical trees, identical JSON reports.

For a human to decide

  • Payin.billing_fee_amount (string vs. spec's number), real bug, not touched here.
  • Rail missing sepa, looks like a safe one-line follow-up (SEPA bank-account creation already works).
  • The tracking_payment/tracking_transaction/tracking_documents gaps, flagged as needing one reviewed cross-SDK PR, not a per-repo blind append.
  • EstimatedAnnualRevenue's top tier is literally "2500000000_plus" in this SDK vs. spec's "250000000_plus" (one fewer zero), a real value mismatch, direction of the fix needs a human call.
  • Everything else in unmodeled.json, Phase C territory (RFI resource, upload/analyze, the rest of the tracking gaps, the bare-string enum regressions).

https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs

Replaces the AI-driven api-sync pipeline with a deterministic, state-based
patcher (scripts/api-sync/), a curated spec-map.json, and an honest
unmodeled.json inventory of every known gap between the spec and this SDK.

Applies the two changes verified safe: QuoteIn.refund_wallet_address on
CreateQuoteInput, and the missing "portage" banking_partner enum member
(both physical BankingPartner declarations). Bumps to 5.1.0 (enum member
added). Everything else found along the way (tracking_payment/tracking_transaction
gaps, kyc_status, several bare-string enum regressions, one real
string-vs-number type bug on Payin.billing_fee_amount, and more) is recorded
in unmodeled.json with an owner and a specific reason, not silently applied.

Adds sync/sync:check/audit:types/sync:coverage scripts, a CI job wired into
main.yaml (state+map validity, a determinism proof, and a non-blocking
coverage + type-audit report), and rewrites api-sync.yml to run the patcher
instead of Claude, removing CLAUDE_CODE_OAUTH_TOKEN entirely.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
copyFileSync(src, dest) with src === dest (e.g. running --apply --spec
.api-sync/spec-snapshot.json, as the determinism-proof CI step does)
truncated the destination before reading the identical source, deleting
the snapshot. Extracted the refresh into its own module with a same-path
no-op guard, and added tests.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
@BernardoSM

BernardoSM commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues
✅ Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

…ge with the PAT

sync:check as documented in package.json failed for anyone running it locally:
it defaulted to spec-current.json, which only exists on the api-sync-data
branch, while CI passed --spec explicitly. Check, coverage and audit assert
against committed state, so the snapshot is the correct default.

A sync PR merged with GITHUB_TOKEN produces a push that starts no workflow, so
the merge would land on main and never publish to npm.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
@ericviana
ericviana merged commit a6c3b0f into main Aug 4, 2026
11 checks passed
@ericviana
ericviana deleted the eric/deterministic-api-sync branch August 4, 2026 00:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants