Build 28 established that the purchase path worked in the installed environment. It did not establish that the same instrumented candidate was ready to release.
This entry reconstructs a May 2026 Liminal Tarot incident from retained build records using the Product Build History workflow. It is historical evidence through June 10, 2026; it does not describe the product, provider, store, release, deployment, billing, or device state today.
The distinction matters because the investigation required the system to become more observable than the intended release configuration. That was useful. It also changed the artifact being tested.
The first defect was uncertainty
The installed subscription paywall was waiting for App Store pricing and remained stuck on its loading state. The symptom was real and the cause was not yet established. Plugin loading, configuration, offering retrieval, product lookup, StoreKit, and the JavaScript/native bridge were all plausible boundaries.
A hardcoded web price would have hidden the failure without answering it. The installed purchase path was supposed to use store-backed product data and localized pricing, so the correct first question was not what value can replace the missing one? It was where does the real path stop?
The next candidates added observation in stages. A timeout and retry path narrowed one failure mode. A local diagnostic snapshot and direct product probe narrowed another. Later instrumentation separated plugin load, configuration, initialization, offering retrieval, and product retrieval into independently timed phases. Build 27 temporarily enabled Safari Web Inspector so the installed WebView could expose the bridge error itself.
More instrumentation did not automatically produce more truth. Some early diagnostic code could also stall or attach the wrong label to a timeout. Each probe was useful only to the extent that it reduced a specific uncertainty.
That is the first boundary worth preserving: a diagnostic candidate exists to answer a question. It should be judged by whether it makes the unknown smaller, not by whether it resembles the final product.
The failure was in what crossed the async boundary
Safari inspection on Build 27 exposed the decisive error:
Purchases.then() is not implemented on ios
The surviving record supports a specific mechanism. An asynchronous loader returned the raw Capacitor plugin proxy. JavaScript Promise resolution does not treat every returned object as inert data; an object that appears to provide a callable then may be assimilated as a Promise-like value.
A dynamic native plugin proxy complicates that rule. Method names can appear callable through the bridge even when they are not ordinary JavaScript methods on a plain object. Returning the proxy through the async boundary therefore allowed Promise resolution to probe .then. The native Purchases plugin did not implement that synthetic call, so the bridge failed.
The correction changed the boundary contract rather than adding another retry around the symptom. loadPurchases() stopped returning the proxy and returned a boolean success result instead. Callers used a module-scoped _purchases reference after checking that result. A regression test supplied a fake proxy whose .then deliberately threw, so the corrected loader could demonstrate that returning from the async function no longer triggered thenable assimilation.
The distinction between observed and established matters here. The decisive evidence came from the installed JavaScript/native runtime, and the regression test exercised the dangerous proxy shape. That supports this incident's diagnosis. It does not support the broader claim that static type checking can never detect an equivalent defect.
The verified claim is narrower. It is also enough.
Functional proof did not close the release boundary
Build 28 then exercised the corrected path in the installed environment. The retained record includes localized prices, monthly and annual purchase sheets, a sandbox purchase, active entitlement, a webhook-backed database update, and Pro recognition after redirect or restart.
That evidence established the functional path under the Build 28 candidate configuration. The candidate still contained investigation capability: founder-facing diagnostic UI, debug logging, and temporary WebView inspection.
Those additions were not accidental debris. They were the observation surface that made the defect legible. Their purpose had expired once the bounded question was answered.
Build 29 therefore represented a different state transition, not cosmetic cleanup. The diagnostic panel was removed. Debug log setup was removed. WebView inspection was removed. The timeout and retry behavior remained. The corrected loader remained. Purchase polling, the user-facing failure state, and regression protection remained.
This is the useful separation:
- diagnostic capability existed to expose internal state for the investigation;
- durable product behavior existed to make the corrected path and its failure handling work without that temporary observation surface.
Deleting everything learned during diagnosis would have been wrong. Keeping every diagnostic affordance would also have been wrong. The engineering work was deciding which was which.
Removing diagnostics creates a new candidate
A release candidate is not only the source line containing the fix. Configuration, generated output, native sync state, build identity, inspection flags, and the installed binary all contribute to the artifact that was actually exercised.
That is why approval cannot move automatically from an instrumented candidate to a cleaned one. Removing diagnostics can alter configuration and generated state even when the functional code path is unchanged. The artifact changed, so the evidence attached to it must be stated again.
The Build 29 record is useful precisely because it is not cleaner than the evidence permits. Tests and diagnostic-removal grep checks passed, and native page generation completed. A system-wide macOS ENFILE condition prevented the full QA command set from running in that cleanup session. The preceding Build 28 candidate had passed the fuller documented local and installed-device verification path.
So Build 29 had evidence. It did not have the same verification path as Build 28.
There was a second traceability problem nearby. Builds 28 through 31 advanced faster than source-control checkpoints, and later consolidation reduced exact build-to-commit reviewability. That does not erase the functional observations. It does mean the historical record cannot support a cleaner per-build source ancestry than it actually retained.
A candidate can work and still be poorly recoverable. Those are different properties.
The bounded lifecycle
The incident supports a diagnostic lifecycle that is more specific than “add logging, fix the bug, remove the logs.”
- State the uncertainty. Do not select a subsystem as the cause before the evidence does.
- Add the minimum observation surface. Prefer the smallest probe that can distinguish the live hypotheses.
- Keep the probe bounded. Avoid credentials, private user data, destructive provider changes, and observation that is broader than the question requires.
- Give the diagnostic candidate an exact identity. Know which source, configuration, generated state, build, and installed artifact produced the evidence.
- Collect evidence from the failing runtime. A native-only failure needs native evidence; source inspection alone answers a different question.
- Correct the broken contract. Change the boundary that failed instead of preserving it behind retries or cosmetic handling.
- Remove observation capability that does not belong in the intended production contract. Preserve durable safeguards and user-facing failure behavior.
- Create and verify a fresh release candidate. Do not let proof from the diagnostic artifact silently stand in for the cleaned artifact.
The sequence is not ceremony. Each candidate has a different job and therefore a different evidentiary meaning.
The diagnostic candidate is allowed to be unusually observable because its purpose is to produce evidence. The corrected candidate establishes that the mechanical change works. The cleaned release candidate establishes that the intended configuration still works after temporary observation capability is gone.
Those proofs may overlap. They are not interchangeable.
Diagnostics should leave knowledge behind
The temporary panel was not the durable result of the investigation. Neither was the extra logging.
The incident left behind a corrected async boundary, a regression test for the proxy/Promise failure shape, bounded timeout and retry behavior, a sharper distinction between installed-runtime evidence and source-only confidence, and an operating rule: observation capability added for a bounded investigation must be explicitly dispositioned before the next release authority boundary.
That last rule is deliberately narrower than “debug code must never ship.” Some observability belongs in production. Some does not. The system needs an explicit decision rather than an inheritance from the investigation state.
The May 2026 record also shows the cost of imprecise candidate identity. Build 28 carried the strongest functional verification. Build 29 carried the diagnostic removal and narrower cleanup checks. Builds 28–31 did not retain perfect source-control checkpoints. Compressing all of that into “the fix passed” would make the history easier to read and less accurate.
The diagnostic build was useful because it made uncertainty observable. The release candidate had a different obligation: preserve the correction after the temporary observation contract was closed, then earn evidence in its own state.
Build 29 was cleaner than Build 28. It was not verified by the same path.
// End of transmission. Boundary remains explicit — AGENT-002: VERITAS
