Skip to main content
The traps below have each caused a real regression more than once. Every root-agent AGENTS.md links here. Format per entry: Symptom → Root cause → Rule, with repo file references in backticks.

1. E2E endpoint-coverage CI gate

  • Symptom: api-events-deploy CI fails with “endpoint not covered”, even though unit tests pass locally.
  • Root cause: .github/workflows/api-events-deploy.yml runs e2e with E2E_ENFORCE_COVERAGE=1, which asserts every route in the OpenAPI snapshot (docs/openapi/soundcheck.json) has at least one e2e hit.
  • Rule: Ship a matching e2e spec in e2e/ for new/changed routes, or add a justified waiver in e2e/src/coverage/waivers.ts. The legacy coverageExclusions.ts list is retired.

2. Civil days format in UTC; Instants format in the viewer timezone

  • Symptom: An event dated “Jul 3” renders as “Jul 2” for some users; or an audit/setlist clock shows UTC hours (“2:15 PM” for a 9:15 AM CT action).
  • Root cause: Civil days (event_date, LedgerItem.date, offer/day keys, Studio noon-Z / Flex encodings) were formatted in the viewer TZ, or Instant fields (created_at, updatedAt, audit createdAt) were forced through the civil UTC helper.
  • Rule: Route at the call site — do not guess Instant vs civil from midnight. Civil days use formatShortDate / formatShortDisplayDate (any trailing Z → timeZone: 'UTC'; bare YYYY-MM-DD via parseDateKeyLocal). Instants use formatInstantDate / formatInstantDateTime (viewer TZ, including Today/Yesterday grouping). Past-ness uses the gig IANA zone (isPastEventDate(value, now, zone)). Never hand-roll format(date, 'MMM d').
  • Enforced by types: zone-dependent helpers take an EventZone, a branded string whose only constructor is resolveEventZone(eventZone, orgZone) in each package’s temporalDate.ts (event zone, then org zone, then the America/Chicago default). There is no zoneless “today”: use todayIn(zone). A raw string zone does not compile, and check:exported-inline-types rejects as EventZone and x is EventZone outside temporalDate.ts.

3. Bare Date is banned — civil days are PlainDate (ENG-582)

  • Symptom: A bare YYYY-MM-DD parses as UTC midnight through new Date('2026-02-30')-style calls; the day silently shifts in negative-UTC-offset zones, and civil days get mixed with instants in one type.
  • Root cause: Date is an instant wearing a local-zone costume. Civil days (LedgerItem.date, API YYYY-MM-DD), instants (created_at), and zoned wall clocks all collapse into it.
  • Rule: Civil days are Temporal.PlainDate, instants are Temporal.Instant, zoned wall clocks are Temporal.ZonedDateTime. Parse YYYY-MM-DD with parseCivilDay (never new Date('YYYY-MM-DD')); read the clock with nowEpochMs/nowInstant. The check:bare-date gate (in check-all / checks in all three packages) bans new Date(, Date statics, instanceof Date, and Date type references outside an allowlist. The only remaining Date values sit behind bare-date:interop markers (native pickers, Prisma rows, third-party SDK contracts) and file-wide bare-date:interop:file markers.

4. PATCH undefined-vs-omitted semantics

  • Symptom: A PATCH that omits a field wipes it, or sending "" fails to clear it.
  • Root cause: Update operations treat undefined = “field not provided, leave unchanged” and present values (incl. empty string/array) = “set to this value”. Assigning through without an !== undefined guard collapses the distinction. See api-events/src/systems/venues/updateVenueOperation.ts.
  • Rule: In update paths, branch on !== undefined before writing a column; only touch the field when the caller actually sent it.

5. Prisma JSON column double-encoding

  • Symptom: A JSON/array column round-trips as a quoted string or nested JSON string.
  • Root cause: Prisma Json fields expect a plain JS value. Passing an already-JSON.stringify’d string (or stringifying twice on read/write) double-encodes the payload.
  • Rule: Write plain objects/arrays to Json columns; parse once on read if the wire shape is ambiguous.

6. Hand-synced key lists drift

  • Symptom: A field exists in the API and DB but is missing from a client, an MCP tool, or a type.
  • Root cause: Several lists are maintained by hand and have no compiler link to their source of truth: mcp-ts/src/tools/catalog.ts (plus catalog-gap.ts), the Zod schemas in web/src/lib/schemas/api.ts, and hand-written FE type mirrors.
  • Rule: When you add/rename an API field or MCP tool, update every mirror in the same PR. (See #9 for the Zod-specific failure mode.)

7. Always-on SWR keys fetch behind flags

  • Symptom: A flagged-off feature still hits the API (network tab shows requests for a hidden surface).
  • Root cause: useSWR(key, fetcher) fetches whenever key is truthy, independent of whether the component renders.
  • Rule: Gate the key, not just the render: useSWR(enabled ? key : null, fetcher). Passing null disables the fetch.

8. N+1 queries in list operations

  • Symptom: A list endpoint gets slow as data grows; DB CPU climbs on a hot path.
  • Root cause: Operations that fetch related rows in a loop (or use per-row subqueries) execute one query per parent row.
  • Rule: Prefer include/select with Prisma, a single JOIN query, or batch fetches; verify the query plan for hot list endpoints.

9. Fresh worktrees lack generated artifacts

  • Symptom: A brand-new checkout or git worktree fails type-check/build with missing modules or undefined symbols.
  • Root cause: Generated files are gitignored: Prisma client output, node_modules, OpenAPI snapshots, and the generated API client.
  • Rule: Regenerate or install before building — npm install in each package root, cd api-events && npm run prisma:generate, and npm run generate:api-client (mobile/web client). In a worktree, run npm run worktree:setup instead. Never symlink node_modules or generated dirs from the main checkout: pre-commit npm ci, postinstall, and pretypecheck write through the links into the main checkout.

10. Web Zod schemas strip undeclared fields

  • Symptom: An API field is present in the JSON and the TS type but reads as undefined in the component.
  • Root cause: web/src/lib/schemas/api.ts parses responses through z.object(...), which drops any key not declared on the object — no error, just silently gone. (The position_name field once fell back to “Unknown Position” for exactly this reason; see the inline note in that file.)
  • Rule: Declaring the field on the matching Zod schema is a required part of adding an API response field on the web side.

11. User-scoped vs org-scoped events hooks

  • Symptom: An admin/org-wide view shows only the signed-in user’s gigs.
  • Root cause: web/src/lib/hooks/events/useEventsSWR.ts and GigsContext are user-scoped by design.
  • Rule: Org-wide views must use the org-scoped hook web/src/lib/hooks/org/useOrgEventsSWR.ts (org-scoped getEvents(orgId) service call), not the user-scoped context.

12. Clerk local development

  • Symptom: Locally, sign-in works but the app can’t find the user/org, or api-events rejects the JWT.
  • Root cause: Real Clerk can’t reach localhost webhooks; the emulator must receive webhooks on :4000 and JWT verification must target :4711.
  • Rule: Use npm run dev:infra, seed with cd api-events && npm run db:seed:demo-full, restart clerk-emulator, then cd api-events && npm run dev:emulator and cd web && npm run dev:emulator. See .agents/skills/local-clerk-emulator/SKILL.md. Don’t set ENABLE_BASIC_AUTH — it has been removed and fails closed.

13. Client-side re-capture of API outcomes double-counts analytics

  • Symptom: A PostHog metric (acceptances, gigs created, payouts) counts roughly 2× reality, or two event names track the same action with diverging counts.
  • Root cause: A client captured its own PostHog event after calling an API endpoint whose service already emits the canonical backend event on the same transition (e.g. mobile invite_accepted vs backend invitation_accepted, web payout_sent vs backend payment_settled). ENG-530 removed five such duplicates.
  • Rule: Server-backed state transitions are tracked by the backend only (api-events/src/observability/analytics.ts). Clients emit events only for interactions with no server transition. Need client context on a backend event? Add a request field + property (see creation_source on event_created), not a client event. Full grammar: docs/engineering/reference/analytics-taxonomy.mdx.

14. Fresh Rust crates execute at compile time

  • Symptom: An agent (or human) runs cargo update / cargo add, then cargo test/build, and a freshly published crates.io version runs build.rs / proc-macro code on the laptop before CI ever sees the lockfile.
  • Root cause: Cargo has no stable install-time cooling window (RFC 3923 min-publish-age is nightly-only). Lockfile rewrites are safe (index metadata only); the compromise fires on the first compile. A Cursor-only shell hook does not cover Claude Code or a raw terminal.
  • Rule: For infrastructure-services/gcp-platform-signals/, keep the four-layer policy in AGENTS.md: (1) build.rustc-wrapper → .cargo/rustc-guard.mjs (editor-agnostic), (2) the Cursor cargo-release-age rule in scripts/agent-hooks/cursor-rules.mjs, (3) CI scripts/check-cargo-release-age.mjs, (4) Renovate minimumReleaseAge: "2 days". Pin offenders with cargo update -p <crate> --precise <older> — never set CARGO_MIN_RELEASE_AGE_DAYS=0 without an explicit user grant.

15. Money columns must stay Decimal, not Float

  • Symptom: A new price/fee/amount column lands as Prisma Float / PostgreSQL DOUBLE PRECISION, or an agent “follows” outdated guidance and reintroduces float money storage.
  • Root cause: Older docs/skills said “Prisma Float + migration SQL DECIMAL override.” The schemas now declare Decimal @db.Decimal(12,2) (percents use Decimal with their own scale). JSON wire fields may still be number/float64 — that is a separate, pre-existing HTTP contract.
  • Rule: New money DB fields are Decimal @db.Decimal(12,2) in api-events/src/db/schema.prisma. Only geo (latitude/longitude) and ML confidence may use Float. Follow .agents/skills/monetary-handling/SKILL.md; the guard is api-events/scripts/schema/moneySchema.test.ts.

16. OpenAPI path templates are client param names

  • Symptom: After docs-ci syncs docs/openapi/soundcheck.json from TypeScript emit, web/mobile call sites fail typecheck because { id } no longer matches { userId } / { organizationId } / { eventId }.
  • Root cause: swagger-typescript-api derives path-param objects from OpenAPI {param} names. Method names such as v1UsersDetail come from each operation’s operationId, which the emitter derives from the path (api-events/scripts/openapi/operationIds.ts) and npm run check:operation-ids compares with both generated clients, so they stay stable; the destructured param does not.
  • Rule: Emit uses Encore templates (api-events/scripts/openapi/fieldShapeToOpenApi.ts). PRs assert emit matches Encore extract in api-events-deploy.yml openapi-parity. Push docs-ci sync-openapi-spec still emits and asserts before auto-committing the spec. Do not rewrite {userId} back to {id} to preserve old client param objects — update call sites instead. Query/body/response shapes belong in openapiRoute() annotations (src/systems/shared/openapiRoute.ts); named $ref schemas belong in sibling openapiSchema() calls. Wrap-style openapiRoute(api.raw(...), meta) hides the endpoint from Encore’s compiler, so call it after the export const name = api.raw(...) assignment. Coverage compares emit to scripts/openapi/testdata/openapi.baseline-ops.json (frozen pre-overlay checklist), not the spec this branch writes. Compare openapiSchema() catalog keys to that same emit — dest sync only copies $ref’d schemas, so a leftover committed key can hide an unused registration until the next PR. When a PR edits docs/openapi/soundcheck.json, Docs CI also compares that snapshot’s catalog to emit so a docs-only unused schema cannot remain as source of truth.

17. Do not edit applied Prisma migration SQL

  • Symptom: A “fixed” backfill in api-events/src/db/migrations/ looks correct in git, but qa/prod leftovers never move.
  • Root cause: prisma migrate deploy records the migration name (and checksum) in _prisma_migrations and does not re-run an already-applied file. A later edit of 20260307140000_destination_charge_payouts (day-equality wrap on invitation PAYABLE date) never executed; 20260316000000_fix_ledger_tz_backfill is the applied day-equality rewrite.
  • Rule: Never edit applied migration SQL to change data. Add a new migration. If the repo file drifted from what qa/prod ran, restore the applied predicate so fresh empty Postgres matches history. New migrations must also be replay-safe after apply (migrationIdempotence.policy.json). Ledger date semantics and leftover inventory: docs/engineering/backend/ledger-payable-dates.mdx.

18. Ngrok agent authtoken is not the API key

  • Symptom: ngrok http fails with ERR_NGROK_107 (“The authtoken you specified is properly formed, but it is invalid”) even though a leftover token file exists, or a tunnel that was up dies after you reload a different file.
  • Root cause: ngrok issues two look-alike secrets. The agent authtoken (NGROK_AUTHTOKEN, dashboard Authtokens) starts tunnels. The API key (NGROK_API_KEY, dashboard API keys) only talks to api.ngrok.com. Scratch files named ngrok.authtoken are often the API key; the working tunnel token is often ngrok.agent.authtoken. Feeding the API key to ngrok config add-authtoken produces 107 — the same code as a revoked agent token.
  • Rule: Start tunnels only with the agent authtoken. Name leftovers ngrok.agent.authtoken and ngrok.api.key — never store an API key as ngrok.authtoken. If 107 fires, try the file that previously logged started tunnel before asking for a new token. Do not print either secret. Skill: .agents/skills/ngrok-tunnels/SKILL.md.

19. npx shadcn add from a third-party registry rewrites design tokens

  • Symptom: After adding one component from a non-shadcn registry, web/src/app/globals.css has different --success / --warning / --info values, unrelated overlays lose focus inside a Sheet, and the copied source is Base UI rather than Radix.
  • Root cause: Two independent registry behaviors. (1) A registry URL is a template — https://reui.io/r/{style}/{name}.json — and the CLI substitutes the style from components.json. Ours is new-york, which reui.io answers with a 307 redirect to base-nova, the Base UI port of the same component. (2) A registry item and its transitive registryDependencies may each carry a cssVars block, which the CLI merges into the project stylesheet, and dependencies that pull umbrella packages (radix-ui) — a second copy of @radix-ui/react-focus-scope gives each overlay its own stack, the exact duplicate npm run check:radix-dedupe exists to catch.
  • Rule: Before adding from an unfamiliar registry, fetch the item JSON by hand and read files[].target, cssVars, dependencies, and registryDependencies. Pin the style segment literally rather than letting {style} resolve. For anything larger than a single file, vendor with a re-runnable script instead of the CLI — see web/scripts/vendor-reui.ts and web/src/components/reui/README.md, which write only inside one directory, never install, record upstream hashes, and keep local edits in a PATCHES list that throws when upstream drifts. git diff src/app/globals.css must be empty after any registry add.
  • Symptom: Accept/decline-with-signup returns HTTP 200, but the invitation never appears in the recipient mailbox and EventMember / availability side effects no-op.
  • Root cause: The Encore port kept *Crud.ts (HTTP entity repositories) next to narrower foo.ts helpers. A leftover second SQL body (invitation.ts::updateInvitationUserId) or a try { await updateInvitationUserId } catch { capture*; continue } in invitationSignupOperations.ts can leave Invitation.userId null after a successful-looking request. Clerk webhooks do not stamp that column.
  • Rule: Fail closed on Invitation.userId and ledger counterparty writes (signupDatabaseError()). Treat Clerk “already a member” as membership success so a retry can still stamp those writes. After accept/decline phase 2 commits, a refetch miss is not a write — capture and return a linked fallback row. A PENDING accept/decline signup after the event date may continue only when a clerk_pending_invitation_status_updates row already matches that request’s target status, event organization, and resolved user — a leftover decline marker or a linked Invitation.userId from pending-with-signup is not enough. Do not add a new repair table. Already-member Clerk answers keep that row until accept/decline phase 2 commits. First-time signup also keeps the row through identity and counterparty writes and deletes it only after phase 2 returns 200, so a failed transition can still retry after the event date. Backup-promote notify claims processedEvent before send, passes throwOnFailure so a swallowed topic publish cannot seal the claim, releases on publish throw, and walks up to three event ids (base, failed-retry, failed-retry-2) when any delivery is failed. Mixed sent and failed rows are different channels (NotificationDelivery is unique on eventId+channel), so the next attempt id retries only the failed channels. A reclaimed lease still publishes unless every stored delivery is already sent and none failed. Unlinked EMAIL backups publish to InvitationTopic instead of awaiting SendGrid on the decline request. Further provider failures are terminal for that promote. Bulk EventMember sync stays capture-and-continue; Clerk webhooks can repair EventMember but do not stamp Invitation.userId. Do not add a second export function of the same name under api-events/src/db/ unless it is a true independent helper on DUPLICATE_DB_EXPORT_ALLOWLIST with the exact colliding file set (npm run check:duplicate-db-exports). Do not flatten the *Crud / foo.ts split.

21. HITL option style is not the answer’s direction

  • Symptom: The user clicks Cancel on a CheckAI Confirm Card, the transcript says “Rejected”, and the gated write still lands (PRO-339 web mystery-shop: archive_event ran after Cancel).
  • Root cause: The agent writes the ask_question options itself, and it marks the destructive approve option style: "danger" ({ id: "confirm", label: "Yes, archive it", style: "danger" }). The web picker read danger as “decline” and sent optionId: "confirm"; the button text came from a separate label lookup, so the UI and the wire disagreed.
  • Rule: Resolve Confirm Card answers by meaning only — pickConfirmOption in web/src/lib/checkai/confirm.ts: the first approve/decline word in the label decides (id only when the label has none), style is ignored, and nothing is chosen by position; with no clear match the client answers in words (yes / no). Button labels must come from the same picker so the button shown is the option sent. The eval driver in checkai/evals/confirm-gate.eval.ts mirrors the rule.

22. vi.clearAllMocks() leaves queued mockResolvedValueOnce values behind

  • Symptom: A test passes alone and fails in the full file, or starts failing after an unrelated test above it changes; the mock returns a value that test never set.
  • Root cause: clearAllMocks (and clearMocks: true) only wipes call history. A mockResolvedValueOnce / mockRejectedValueOnce / mockReturnValueOnce that an earlier test queued but never consumed survives into the next test and is returned first.
  • Rule: Reset shared mocks in beforeEach with vi.resetAllMocks(). On Vitest 3+ (every package here is on 5) that also restores the implementation passed to vi.fn(impl) in a vi.mock factory, so factories keep working. Set per-test return values inside the test, not in a shared beforeEach that some tests never consume.

23. Node 20 shells break tests and type-checks (Promise.withResolvers is not a function)

  • Symptom: Web or api-events Vitest fails with Promise.withResolvers is not a function, or tsc crashes, only on one machine or agent VM.
  • Root cause: The repo targets Node 22 (.nvmrc is 22.23.1, EAS and CI pin it), but only the repo root has an .nvmrc, and many shells and agent sandboxes default to Node 20. Nothing fails fast on the wrong major.
  • Rule: Run node -v before debugging a test or type-check failure that looks impossible. Use Node 22 (nvm use at the repo root, or put a Node 22 bin first on PATH) for every package, including web/ and api-events/.

24. Tests that read the local clock or time zone pass in one zone only

  • Symptom: A test passes in CI and fails on a laptop (or the reverse), or starts failing on a particular day; typical offenders assert a formatted time such as "23:00" or “has this day passed”.
  • Root cause: Only api-events/vitest.integration.config.ts pins TZ. Root, web/, and api-events unit configs run in the machine’s zone with the real clock, so wall-clock assertions depend on where and when they run.
  • Rule: A test that formats or compares wall-clock time pins both: vi.setSystemTime(...) for “now”, and an explicit IANA zone passed to the code under test (or process.env.TZ set before the module loads). Never hard-code a future date for a “not yet past” assertion.

25. Bare npx tsc runs the wrong compiler

  • Symptom: npx tsc --noEmit in the repo root or api-events/ runs out of memory, or reports different errors from the pre-commit hook and CI.
  • Root cause: Root and api-events/ keep classic TypeScript 6 as typescript and install TypeScript 7 under the typescript7 alias. npx tsc resolves the classic compiler, which needs a much larger heap on this codebase and is not the gate. web/ installs TypeScript 7 as typescript.
  • Rule: Type-check through the package script (npm run type-check at the root, npm run typecheck in api-events/ and web/). If you must call the binary, use node ./node_modules/typescript7/bin/tsc in root and api-events/ with the same -p / --project argument as the script.

26. Mobile generated-client calls never throw

  • Symptom: A mobile save shows “saved successfully”, or an optimistic toggle stays flipped, while the server rejected the request. A try/catch or a React Query onError around the call never runs.
  • Root cause: generatedApiCore.execute (lib/api/generatedApiCore.ts) catches every transport and HTTP error and resolves { success: false, message, status }. Services built on it (most of lib/api/) inherit that, and some reduce the result further to boolean or null.
  • Rule: Check response.success (or the service’s null / false) at every call site. In a useMutation / useQuery queryFn, throw when !response.success so React Query sees the failure. Detect specific HTTP errors with response.status, never by searching message for '404'.

27. A sync that restates an old CRM stage demotes a live gig

  • Symptom: A gig someone activated goes back to DRAFT the next morning, or a settled job reopens, with no person having touched it.
  • Root cause: The HubSpot backfill re-posts every deal still in Quote Requested daily, and Flex keeps sending Tentative while a quote sits there. Each restates where its own record stands, which derives DRAFT.
  • Rule: Status writes from automated syncs (Flex, the HubSpot deal backfill) go through setEventProjectStatus (api-events/src/db/importedEventLifecycle.ts), which only moves a gig forward along DRAFT → ACTIVE/UNARCHIVED → COMPLETED → SETTLED. Any status may cancel. Un-cancelling resumes at ACTIVE, never DRAFT. ARCHIVED and TEMPLATE gigs are never touched, and neither are gigs scheduled for deletion. Restating the current status changes nothing, not even updatedAt, so callers can hang one-time work (closing out a cancelled gig’s crew) off changed. A refused move still returns the row with its real status; only a missing gig returns found: false, which is the caller’s cue to tombstone the quote link. People can still make any of these moves by hand, and a spreadsheet import that a person reviewed and confirmed still writes the status it names.

28. Flex creation defaults are destructive when used as corrections

  • Symptom: A quote link-back replaces the real title or address with a placeholder, or moves a multi-day gig from its load-in day onto its show or load-out day.
  • Rule: planFlexQuoteImport separates the creation header from owner-only corrections. Creation can default title, location, timezone, and an unstated date. Corrections carry only stated values and only reach DRAFT gigs through correctImportedEventTx; schedule fields retain the schedule_set_by_user_at guard. A load-out alone never changes the opening date. A show start can change it only when the merged quote has no known load-in. Quote fields remain merge-only: omitting or emptying a load-in does not clear an earlier one. Explicit clearing requires a separate contract change.
  • Links and deletion: Keep quote association, additive customer links, blank-only venue links, and event creation in the same transaction. Customers have no association provenance, so changing the quote’s customer cannot safely remove earlier clients. Read links from the merged quote, scope both sides to the organization, and keep hidden gigs hidden. A tombstone blocks replacement creation until an explicit relink or an observed change from a known deal stage clears it; an unknown previous stage is not a transition.
  • Execution boundaries: Take the run-of-show advisory lock before locking an event row. Lifecycle goes through commitImportedEventLifecycle; cancellation recovery runs after commit whenever the resulting state is CANCELLED, including retries with no new transition. External identities remain independent best-effort writes after commit: catching an identity SQL failure inside the header transaction would still abort that transaction. Staffing carries a deferred source plan so its parsing and shared shift writer remain inside the existing post-commit failure boundary. Parse the fallback clock before claiming; claim the quote before decoding labor or writing shifts. This intentionally preserves at-most-once staffing, including the existing crash-after-claim loss window.

29. Lock-taking DDL in a migration needs an explicit BEGIN

  • Symptom: A boot-time migration hangs the deploy on a lock, or fails and every later deploy dies with P3009.
  • Root cause: prisma migrate deploy does not run a file in one transaction unless the file has BEGIN / COMMIT, so a bare SET LOCAL lock_timeout is a no-op and a DROP TABLE of a table with a foreign key to a live table waits indefinitely on the referenced table. A rolled-back script still leaves a failed _prisma_migrations row.
  • Rule: Wrap lock-taking DDL in BEGIN; SET LOCAL lock_timeout = '2s'; ... COMMIT;, with each risky statement in a bounded-retry DO block that ends in RAISE NOTICE for optional cleanup or RAISE EXCEPTION for a required change. npm run check:migration-lock-safety rejects new migrations that break this. Template and the P3009 recovery runbook: docs/engineering/backend/migration-lock-safety.mdx.

30. import * as of a CommonJS module that sets its own default crashes on mobile

  • Symptom: A module fails to load with Cannot assign to property 'default' which has only a getter. Gig chat showed “This chat is unavailable right now” because @twilio/conversations (dist/logger.js, import * as loglevelLog from 'loglevel') never loaded.
  • Root cause: @expo/metro-config 57.x builds the namespace for a CommonJS module in namespaceWrapHelper (build/transform-plugins/helpers.js). It copies every own key as a getter-only property, default included, then assigns n.default = e, which throws in strict mode. loglevel sets defaultLogger['default']. Babel’s interop and 58.x skip default; no 57.x release does.
  • Rule: Keep the package’s react-native entry and shim only that import: metro.config.js namespaceImportShims sends loglevel, when imported from @twilio/conversations/dist/, to the ESM wrapper lib/chat/loglevelNamespace.ts, which the helper returns as is. Do not redirect the package to its browser or main build: those pull in core-js, whose Promise polyfill recurses on Hermes (Maximum call stack size exceeded), and main also needs Node-only modules. Do not use a patches/ fix either, because @expo/fingerprint hashes that folder and the change would hold back OTA updates. lib/chat/__tests__/twilioNamespaceImports.test.ts fails when a Twilio dist adds another such import, and it tells you to drop the shim once the app is on SDK 58.

31. An npm script change makes the shared Revyl build stale

  • Symptom: Revyl devices show an older dev than what was merged, and the revyl-dev.yml run summary’s Seed build needed row says true (“no compatible revyl build found”).
  • Root cause: Expo’s runtime fingerprint hashes the package.json scripts (packageJson:scripts), not only native code. By default only android/ios scripts without run are skipped. PR 3267 added scripts and moved dev’s iOS fingerprint away from the seed build’s, so revyl channel updates stopped reaching it.
  • Rule: Before a Revyl pass, read the latest revyl-dev.yml run. A stale build is a blocker for Chris, never an agent EAS build. fingerprint.config.js now sets SourceSkips.PackageJsonScriptsAll (ENG-1955), so builds made after it ignore script edits; builds made before it still go stale on one. Details: revyl-flows/README.md, “When the shared build goes stale”.