Repository navigation
fix(firebase): declare the composite indexes cortex's queries need [TURNWRK-775] - #45
Conversation
…URNWRK-775] Symptom: cortex's "Cron: Trade reminder sweep" (*/15) failed every run on inngest.practical.works since at least 2026-10-08 (78/78 that day, 27/27 on 2026-10-18), with `9 FAILED_PRECONDITION: The query requires an index`. No T-24h / T-2h appointment_reminder text has gone to a trade customer since. Mechanism: tradeReminderSweep.ts queries cmms_workOrders with status == 'Scheduled' plus a scheduledDate range. That needs a composite (status ASC, scheduledDate ASC); the error's console link decodes to exactly that, COLLECTION scope. firebase/firestore.indexes.json here is the only file that deploys to project turnwrk (deploy-firebase.yml); the copies under each app's packages/shared/firebase are vendored for emulators. The live index list matches this file, and it never had the pair. The same sweep of cortex found two more compound queries with no index: cmms_workOrders (assignedTechId ==, orderBy createdAt desc) and resupplyRequests (cleanerId ==, orderBy createdAt desc), in smsPhoneResolver.ts and telegramUserResolver.ts. Both catch the error and log a warning, so inbound SMS / Telegram sender context has silently lost its "most recent work order / property" hint. Declared both. Rejected: creating the index from the console link. It fixes prod and leaves the file stale, so the next `--force` index deploy would drop it. Proof: the Firestore emulator serves compound queries without indexes, so no emulator test can go red here. tests/firestoreIndexes.test.ts asserts the three declarations directly; with the JSON change stashed it fails 3/3. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
practical-bot
left a comment
There was a problem hiding this comment.
Review
The diff is narrowly scoped to the canonical deploy source and a declaration guard. The three entries use COLLECTION scope and the exact field order/directions required by the cited queries. The test checks collection group, scope, field count, field paths, and ordering rather than merely checking that a name appears. No UI files changed, so the committed-screenshot requirement does not apply.
PR-body claim ledger
- “
cmms_workOrders (status ASC, scheduledDate ASC): the sweep.” PASS. The entry is present infirebase/firestore.indexes.json;tests/firestoreIndexes.test.tspasses its corresponding assertion. - “
cmms_workOrders (assignedTechId ASC, createdAt DESC)andresupplyRequests (cleanerId ASC, createdAt DESC).” PASS. Both entries are present and their corresponding assertions pass. - “This file…is the only prod deploy.” PASS by repository inspection.
firebase.jsonpoints atfirebase/firestore.indexes.json, and.github/workflows/deploy-firebase.ymlis the owner-triggered index deployment path. - “The vendored copies reach parity through the usual
bot/sync-sharedPRs, and consumer CI's integrity check enforces it.” PASS by repository inspection / CI. The workflow and the passingcheck:vendoredstep support this claim. - “No backlog guard added, because none is needed.” UNVERIFIED. The cited cortex implementation is outside this repository and no live cortex checkout was available in this review; this is a scope rationale, not a changed behavior in this PR.
- “Failure alerting…is not in this PR…Filed separately.” PASS as scope accounting. The PR changes no alerting code, and the work-item record identifies the separate TURNWRK-779 split.
- “Equality-only compound queries…need no composite.” UNVERIFIED. This requires reviewing the external cortex query set and Firestore production configuration; it is not established by this repository diff. No missing declaration was found within the stated PR scope.
- “Other apps' queries…were not audited.” PASS. The body states the audit boundary explicitly, and this PR does not claim to modify those applications.
- “Exact mutation:
git stash -- firebase/firestore.indexes.json, thennpx vitest run tests/firestoreIndexes.test.tsgives 3 failed / 3.” PASS, using an isolated equivalent mutation. I removed the three required declarations in a temporary copy (leaving the worktree untouched) and rannpx vitest run tests/firestoreIndexes.test.ts; output was3 tests | 3 failed,MUTATION_EXIT=1, with all three assertions reportingexpected undefined to be defined. The unmutated command passed:1 passed,3 passed. - “
npm run typecheckis clean.” PASS. Rannpm run typecheck; exit 0. - “
npx vitest rungives 78 files, 820 tests passed.” VERIFIED-BY-CI. Run39181584877, jobtypecheck + unit, rannpm run test:runsuccessfully on this head. I also ran the changed-surface test directly:npm run test:run -- tests/firestoreIndexes.test.ts;1 passed,3 passed. - “
npm run check:vendoredfails locally only because of BSD sed…CI runs it on ubuntu.” VERIFIED-BY-CI for the relevant claim. Run39181584877,typecheck + unit, completednpm run check:vendoredsuccessfully. - “Index deploys are not CI…owner runs…with
targets=indexesandforce=false.” PASS by workflow inspection..github/workflows/deploy-firebase.ymldefines that manual gate and mapsindexestofirestore:indexeswithout--force. - “The three new indexes show
READY…The nexttrade-reminder-sweeprun…is COMPLETED…” UNVERIFIED-BY-DESIGN. These are post-deploy production checks and cannot be answered from this pre-merge repository. The work item already carriesneeds:verification. - The CI rows were all checked:
typecheck + unit,firestore rules behavior tests, andsecret scanall passed on this head. Thetypecheck + unitjob's steps included typecheck, unit tests, build, email typecheck, and vendored parity.
The acceptance criterion that is in this PR is met: the canonical source declares the required collection-scope composites. The production deployment and one-day green sweep remain post-merge verification, not review blockers. The alerting criterion is explicitly split to TURNWRK-779 rather than silently omitted.
REVIEW VERDICT: approve
NEEDS VERIFICATION: After TURNWRK-778 deploys firestore:indexes to project turnwrk, confirm the three indexes reach READY; then confirm the next trade-reminder-sweep run is COMPLETED and its output contains {scanned, sent24h, sent2h, skipped, errors} with errors: 0.
Why
cortex's
Cron: Trade reminder sweep(trade-reminder-sweep,*/15) has FAILED on every run on inngest.practical.works since at least 2026-10-08 (78/78 that day, 27/27 on 2026-10-18). Run01M58NKH8MWZ2F5MW24WXBJ9H7ends in9 FAILED_PRECONDITION: The query requires an index. So no T-24h / T-2happointment_remindertexts have gone to trade customers since.The query (
turnwrk-cortexsrc/services/tradeReminderSweep.ts:126, origin/mainfc3026f):No
orderBy. The error'screate_compositelink decodes tocollectionGroups/cmms_workOrders, fieldsstatus ASC, scheduledDate ASC, __name__ ASC, COLLECTION scope. Neither the file nor the live project has it: I read the live index list for projectturnwrkand it matches this file, with no(status, scheduledDate).Which file deploys. This file.
firebase.jsonhere points atfirebase/firestore.indexes.json, and.github/workflows/deploy-firebase.yml(owner-triggeredworkflow_dispatch) is the only prod deploy. The copies at<app>/packages/shared/firebase/firestore.indexes.json(cortex, dispatch, whichTurnWrk/cmmsnow redirects to, clean, restock) are vendored for emulators.sync-consumers.ymlrefreshes them from here, and dispatch'sfirebase.jsonsays not to deploy from the app repos. Cortex has nofirebase.jsonat all. So the fix goes here and only here. The vendored copies reach parity through the usualbot/sync-sharedPRs, and consumer CI's integrity check enforces it.What changed (net source lines: +24 JSON, 0 TS; the test file is +55 and excluded)
firebase/firestore.indexes.json: three COLLECTION-scope composites.cmms_workOrders (status ASC, scheduledDate ASC): the sweep.cmms_workOrders (assignedTechId ASC, createdAt DESC)andresupplyRequests (cleanerId ASC, createdAt DESC). Cortex runs both aswhere(==).orderBy('createdAt','desc').limit(1)insmsPhoneResolver.tsandtelegramUserResolver.ts. Both are wrapped in try/catch that logs a warning, and the code comment even says "Composite index might be missing". Neither index is in the file or live, so inbound SMS / Telegram sender resolution has been quietly running without its "most recent work order / property" hint.tests/firestoreIndexes.test.ts: a readable guard that checks each of the three is declared, and records which consumer query needs it.Net positive because these are new declarations. Nothing existing does this job, so nothing could be removed.
What deliberately did NOT change
*/15): that is TURNWRK-776.pickReminderfires only whenapptStart - nowis within ±1h of 24h, or within ±1h of 2h.apptStartis 09:00 org-local onscheduledDate. A past appointment has a negative delta and never matches. An appointment whose 24h window passed during the outage (say 20h out) misses its 24h text and gets only its 2h text, at the right time. Each send writescustomerReminder24hAt/customerReminder2hAton the work order before sending, and checks it before the next send, so dedup is already on the doc. The query range (today-1 .. today+3) is relative tonow. Result: once the index is live, the first run sends at most what would have been due in that 15-minute tick. It sends no burst of stale reminders.onFailure/ alert sink oncronFunctions), it adds shadow(failure)functions to a CONNECT app's registration, and it needs a destination choice. Filed separately (see the card comment).missedCallServicephone+orgId+status,telegramUserResolverorgId+telegramChatId,weather/load.tsorgId+date / orgId+scheduledDate,firestoreAdminorgId+status): Firestore serves these by merging single-field indexes (no field overrides are declared, so single-field indexes are on), so they need no composite. I left them alone.Proof
Guard seen to fail. Exact mutation:
git stash -- firebase/firestore.indexes.json, thennpx vitest run tests/firestoreIndexes.test.tsgives 3 failed / 3.git stash poprestores it, and the run gives 3 passed.The Firestore emulator serves compound queries without composite indexes, so no emulator test can go red for a missing index. That is why this guard reads the declaration directly.
Local run:
npm run typecheckis clean, andnpx vitest rungives 78 files, 820 tests passed.npm run check:vendoredfails locally only because of BSD sed on macOS (\1 not defined in the RE), in a script this PR does not touch. CI runs it on ubuntu.Verification
Index deploys are not CI. After merge, an owner runs Actions -> Deploy Firebase rules -> Run workflow on
mainwithtargets=indexesandforce=false. Additive only, so nothing is dropped. Equivalent CLI from this repo:npx firebase-tools@14 deploy --only firestore:indexes --project turnwrk. Tracked as atype:human-taskcard blocking TURNWRK-775.Then:
READYoncmms_workOrders/resupplyRequestsin projectturnwrk.trade-reminder-sweeprun on inngest.practical.works is COMPLETED with{scanned, sent24h, sent2h, skipped, errors}, anderrors: 0.Refs: TURNWRK-775
🤖 Generated with Claude Code