Skip to content

26.0.1 — Test evidence ​

Evidence is what was observed, not what was expected. A gate output, a query result, a device screenshot. "Should work" is not evidence.

Automated gates ​

Measured against PR #27 (feat/mobile-nativewind-b2 → develop, merged a82c0b4, 2026-08-03/04) and locally against the merged develop tip.

GateResultRun
lint / format:check / type-check✓ passCI workspace job, run 30823165293
test (jest-expo + node --test)✓ pass (566 + 130 tests)CI workspace job, run 30823165293
e2e (web bundle only)✓ passCI e2e-web job, run 30823165293
sonar (SonarQube CE baseline)✓ pass — 0 findings above baselineCI sonar job, run 30823165293
check:parity · check:sql · check:readmes✓ all pass locally (27 migrations, 10 tables + 14 functions commented, all modules documented)local, 2026-08-04
check:release✓ pass — 0 problemslocal, 2026-08-04
check:env-drift · check:fn-confignot run as the script — requires SUPABASE_ACCESS_TOKEN, unavailable in this local shell. Equivalent verification performed directly via Supabase MCP tools (list_migrations, list_edge_functions on both projects) — see CR-03/CR-04 evidence below.
Deno tests (test:ef)✓ pass (13/13, _shared/webhook.ts)CI Edge Functions (Deno tests) job, run 30823164773
pgTAP (test:db)✓ pass — 165/165 assertions, 9 files, from a from-scratch supabase db resetlocal, 2026-08-04 (also CI Database (pgTAP) job)
gitleaks / semgrep✓ passCI, run 30823165293

Cross-platform parity ​

All three surfaces, every release. There is no exception field — parity is unconditional.

SurfaceBuildVerifiedEvidence
Android nativeparity-native.yml build step succeeded (BUILD SUCCESSFUL in 24m52s)✗ not verified end-to-end — the emulator boot step failed twice for reasons unrelated to app code (disk exhaustion, then the emulator never registering as an ADB device). See QRS-337/QRS-338.run 30823164736
iOS nativenot run this cycle (PR-gated cadence is Android-only; iOS runs on uat push / nightly per parity-native.yml's own cost design)——
Web PWAexpo export -p web via e2e-web✓ passCI run 30823165293

This is a real, named gap, not a silent one. parity-native.yml has never once completed successfully on any surface (QRS-295) — its own header comment anticipates exactly this outcome for a first run. Native device verification (Android + iOS physical device, per CLAUDE.md's parity checklist) remains an open item on the owner's device-verification track and is not claimed as done here.

Change-record evidence ​

CR-26.0.1-01 — Branch reconciliation ​

  • What was run: PR #27 (feat/mobile-nativewind-b2 → develop) merged via gh pr merge --merge, 2026-08-03/04, after every code-correctness CI check passed (see Automated gates above).
  • Observed: git log on origin/develop shows merge commit a82c0b4; local develop fast-forwarded cleanly to the same commit with no conflicts. develop and uat/main reconciliation (the remaining half of CR-01's original scope) is deliberately not yet done — tracked separately, not claimed here.

CR-26.0.1-02 — reserved_slugs Prod↔Dev reconciliation ​

  • What was run: is_slug_reserved('privacy'), is_slug_reserved('mumbai'), is_slug_reserved('zomato'), is_slug_reserved('hotel-krushna') executed against Prod via execute_sql.
  • Observed: First three return true, the real merchant slug hotel-krushna returns false. Confirmed functionally, not by row count (per CLAUDE.md's own standard for this exact check).

CR-26.0.1-03 — The five migrations Prod was behind ​

  • What was run: Applied all 5 migrations to qr-setu-prod via MCP apply_migration, one at a time: slug_integrity_trigger_and_reserved_check, seed_platform_reserved_slugs, business_domains_two_level_taxonomy_and_read_rpc, vertical_archetypes_and_domain_archetype_mapping, composable_capabilities_catalog_and_archetype_defaults. list_migrations called immediately after each apply (QRS-267 discipline) — MCP stamped its own timestamp every single time, and each was corrected via UPDATE supabase_migrations.schema_migrations SET version = '<authored-version>' to match the already-applied Dev version and the local filename, before proceeding to the next migration.
  • Observed:
    • select business_domain_id, count(*) from profiles group by business_domain_id before the archetype migration: {1: 9, 5: 1, null: 1} — matched the migration's stated assumption exactly.
    • Same query after: {11: 9, 21: 1, null: 1} — the 9 restaurants and 1 education profile remapped correctly, no profile lost or duplicated.
    • select * from get_business_domains() returns all 12 selectable verticals, each with the correct archetype_id/archetype_key/default_capabilities (e.g. cafe → ecommerce_cart + [catalog,media,leads,orders]; electrician → lead_portfolio + [catalog,media,leads,appointments], correctly excluding orders).
    • list_migrations on Prod post-promotion shows 21 entries, an exact prefix match against Dev's migration list through 20260801091104.

CR-26.0.1-04 — verify_jwt correction ​

  • What was run: For each of the 5 Prod functions (manage-profile, manage-account, manage-settings, manage-reminders, get-dashboard-data): fetched live source via get_edge_function, confirmed in-function auth already exists (Authorization header check + auth.getUser()/requireAuth(), 401 on failure) — the required order per CLAUDE.md ("confirm each function enforces auth in-function BEFORE flipping") — then redeployed the identical, unmodified source via deploy_edge_function with verify_jwt: true. Repeated for the 3 of 5 Dev functions that exist under matching names (manage-profile, manage-account, manage-settings).
  • Observed (live functional probe, not just a config read-back):
    • curl -X POST with no Authorization header against all 5 Prod functions → 401 {"code":"UNAUTHORIZED_NO_AUTH_HEADER","message":"Missing authorization header"} — identical platform gateway error on all 5, distinct from each function's own custom error body (verified from source: manage-profile's own code says {"success":false,"error":"..."}; manage-account's says {"success":false,"message":"..."}) — proof the request never reaches function code.
    • curl -X POST with Authorization: Bearer not-a-real-jwt against all 5 Prod functions → 401 {"code":"UNAUTHORIZED_INVALID_JWT_FORMAT","message":"Invalid JWT"}, again identical across all 5.
    • Same no-header probe against the 3 flipped Dev functions → identical UNAUTHORIZED_NO_AUTH_HEADER gateway response.
    • list_edge_functions on Prod confirms verify_jwt: true on all 5 target slugs.
  • Known, deliberate scope gap: Dev's get-dashboard-data (doesn't exist under that name on Dev) and manage-reminders (Dev has manage-reminder, singular — a different implementation entirely, QRS-286/302) were not flipped. This is not an oversight — it is blocked by the pre-existing Dev/Prod Edge Function drift (QRS-283/286), which is its own, separately-tracked problem. CR-04 is recorded as complete on Prod (5/5), partial on Dev (3/5 possible).

CR-26.0.1-17 / CR-26.0.1-18 — orders and payments ​

(same evidence run) ​

  • What was run: supabase db reset against the local stack, applying all 21 migrations from scratch, then supabase test db.
  • Observed:
    • Applying migration 20260810100000_v2_orders.sql and ...20260810110000_v2_payments.sql both applied with no error; db reset exit 0.
    • Object inventory queried from the live catalog: orders, order_items, payments, payment_events present · 6 policies · 10 CHECK constraints on orders · features registry shows orders -> ledger/scoped and payments -> CORE/universal.
    • The placement test, which is the substance of CR-17. Applicability computed across all 14 industries: orders resolves true for exactly the six goods industries (boutique, dairy, festival_stall, kirana, sweet_shop, tiffin) and false for every time and expertise industry; payments resolves true for all 14, being universal. So festival_stall gains orders with no composition change and real estate cannot be sent one.
    • npm run check:sql exit 0 — 21 migrations scanned, 30 tables + 20 functions + 28 policies + 14 triggers all carrying COMMENT ON.
    • supabase test db exit 0, Result: PASS, Files=2, Tests=69 (up from 45 — 24 new assertions in orders_payments_test.sql).
  • The negatives that matter, asserted rather than assumed: a registered consumer sees the two orders they placed across two different merchants and cannot see the anonymous order sitting beside their own in the same workspace; a consumer with no workspace and no orders sees nothing; neither a merchant nor a consumer can read payment_events at all; anon is refused at the grant boundary before RLS is consulted; a consumer cannot insert an order; a redelivered webhook event is refused by the unique replay guard; and deleting a catalogue item nulls order_items.item_id while leaving the sold name and price intact, which is ADR-0027 D4's snapshot exception under test.
  • ⚠ What this evidence does NOT claim. Nothing has been applied to Dev or Prod — this is a local reset only. No Edge Function, service or client code reads either table yet, so there is no functional probe of an end-to-end order, and payments cannot be switched on for any workspace because payout_accounts does not exist to write its availability grant.

CR-26.0.1-19 — offline payment providers ​

  • What was run: supabase db reset (22 migrations from scratch), then supabase test db.
  • Observed: Applying migration 20260810140000_v2_payments_offline_providers.sql with no error, reset exit 0; supabase test db exit 0, Result: PASS, Files=2, Tests=73 (up from 69 — four new assertions in a new §D).
  • The four assertions, and why each one exists: a cash advance can be captured with no provider payment id (the original constraint would have refused it, making the feature unusable) · a commission on an offline row is refused — and the fixture for that assertion deliberately uses arithmetic that BALANCES (10000 = 200 + 9800), so it proves the new constraint catches what payments_split_adds_up structurally cannot · an online captured payment still requires a provider id, proving the guard was narrowed rather than removed · an unknown provider (paytm) is still refused, proving the CHECK was widened to a known set rather than opened.
  • ⚠ Not claimed: nothing applied to Dev or Prod; no code reads payments yet; and per QRS-482 online collection may be unusable for this vertical regardless of schema.

Device verification ​

Never trust a device result without proving the device runs your code: check pm dump <pkg> | grep lastUpdateTime and confirm Metro logged a bundle build (see CLAUDE.md).

Not performed this cycle. No native device build was produced or installed for this change set — see the Cross-platform parity section above.

CR-26.0.1-20 — Setu Card template registry ​

npm run check:sql — passed, 23 migration(s) scanned; 31 tables + 23 functions + 28 policies + 17 triggers all commented.

supabase db reset --local — exit 0, all 23 migrations applied in order.

supabase test db — exit 0 · Files=3 · Tests=96 · Result: PASS. New file supabase/tests/database/setu_card_templates_test.sql contributes 23 assertions (73 → 96, and 23 is exactly the assertion count in the file, so none was skipped).

⚠ Read the count, not just the colour. The first run reported Files=3, Tests=73 — three files but the same 73 assertions, because the new file aborted on an invalid fixture UUID and contributed zero tests (Wstat: 768 (exited 3) Tests: 0 Failed: 0). The end sentinel could not save it, since the file died before any assertion ran. The signal was the test COUNT failing to rise, which is the QRS-240/245 discipline: summary, exit code AND count, all three.

The assertions that matter, both directions of ADR-0019 lock 2:

Assertion
⭐Retiring a template with a live card pinned to it is REFUSED (P0001)
⭐Once the last card moves off it, the same retirement succeeds — the guard is bounded by live usage, not a permanent prohibition
Deprecating a pinned template succeeds — the move an admin actually needs, and a guard blocking both would get deleted
A retired template disappears from get_setu_card_templates(); a deprecated one does not
A past availability window yields is_selectable = false and the row is still returned (seasonality gates selection, never rendering)
An unknown or retired archetype key is refused; empty archetype_keys is refused
An unknown feature_code is refused by FK (23503)
anon and authenticated hold no table privileges; only authenticated may execute the picker read

CR-26.0.1-21 / CR-26.0.1-22 / CR-26.0.1-23 — order status, chat, chat organisation ​

(same evidence run) ​

(same evidence run) ​

Verified, with the numbers:

GateResult
npm run check:sql✅ EXIT=0 — 26 migrations scanned; 38 tables + 29 functions + 40 policies + 21 triggers all commented
node --test (packages/domain/src/orders, src/hours)✅ EXIT=0 — 35 assertions, 35 pass, 0 fail
npm test -w @qrsetu/domain (full package)✅ EXIT=0 — 215 pass, 0 fail; the pre-existing 180 still green, so the two new barrel exports collided with nothing
npm run type-check -w @qrsetu/domain✅ EXIT=0
npm run check:disk✅ C: 26.4 GB / D: 19.6 GB, both above the 15 GB floor

⚠ check:sql earned its place on this change, twice. The first draft of 20260811100000_v2_chat.sql was rejected for six functions missing their revokes — including the three trigger functions, which are the easy ones to think are exempt. The rule it enforces is not intuitive and is worth restating: CREATE FUNCTION grants EXECUTE to PUBLIC by default, create or replace re-grants it every time, and Supabase's explicit anon grant is not removed by a REVOKE … FROM PUBLIC (QRS-214) — so both revokes must accompany the definition permanently. The second run then rejected two policies with no COMMENT ON POLICY. Neither would have been caught by review; both were caught in under a second.

✅ EXECUTED AND GREEN: supabase/tests/database/chat_test.sql ​

supabase db reset   → all 27 migrations applied, reset_exit=0
supabase test db    → Files=4, Tests=147, Result: PASS, exit 0
                      chat_test.sql ................. ok   (51 assertions)
                      orders_payments_test.sql ...... ok
                      setu_card_templates_test.sql .. ok
                      v2_isolation_test.sql ......... ok

⚠ Read this section for the two failures it took to get there, not for the green line. Both were real defects in the migrations, and one of them was an architecture violation that no amount of reading would have caught.

Failure 1 — the fixture, and the abort mode this file's own header warns about.orders.buyer_name and buyer_phone are NOT NULL ("an order with no contact route is not an order") and the fixture omitted them. The insert failed, the whole file aborted at §B, and the suite reported "All 3 subtests passed" — a green sentence covering 3 of 44 assertions. ⚠ The signal was the test COUNT, not any failure message, exactly as the file header predicted for a bad uuid. Root cause on my side: I read the orders schema by grepping for check (, _at and status rather than reading the column list, so two NOT NULL columns with no CHECK were invisible to the pattern. Grepping a schema is not reading a schema.

Failure 2 — ⚠ AND THIS IS THE IMPORTANT ONE: v2_isolation_test.sql §A reported have: 22, want: 0. The chat migrations had granted 22 table privileges to authenticated. That assertion is not a hardening preference, it encodes the data-access architecture: the app never queries a table directly. Reads go through a SECURITY DEFINER RPC, writes through an Edge Function on the service role, and supabase.from() is banned in app code so table names never reach the network wire. Every other v2 table already followed it — orders, payments, setu_cards and catalog_items all revoke from authenticated and grant it nothing. The grants were the defect; the test was right.

Three things followed from fixing it, and the second would have been very hard to debug in the field:

  1. All 22 grants removed. anon never had any: chat is never anonymous.
  2. ⚠ The realtime.messages policy had to stop reading public.conversations directly. An RLS policy expression is evaluated with the caller's privileges, so once authenticated holds no grant the subquery returns nothing and every channel join is denied — silently. Chat would simply never deliver, with no error anywhere to explain why. It now goes through my_conversation_ids(), which is exactly what the my_* SECURITY DEFINER family exists for, and a new assertion greps the policy expression to prove it stays that way.
  3. 20260811120000_v2_chat_read_api.sql (CR-24) adds the two read RPCs, because an unreachable table is the pressure that makes the next person re-add a grant.

And one assertion tripped over the invariant it was written to defend, which is a small proof the invariant is real: comparing category_label against industries.name failed with "permission denied for table industries", because the comparison ran while role = authenticated. The expected value is now captured as owner into a temp table before the role switch.

The 51 assertions, load-bearing ones first:

Assertion
⭐An authenticated stranger gets zero rows from get_my_conversations(), and get_conversation_messages() raises on a guessed uuid rather than returning empty — an empty page is indistinguishable from a new thread, so returning it would be an existence oracle
⭐Neither anon nor authenticated holds any privilege on any of the seven chat tables, and a real participant still cannot select public.messages directly
⭐The realtime.messages policy exists, RLS is on, and its expression routes through my_conversation_ids() — the only assertions in the repo that would notice any of the three being undone
⭐Block refuses inserts in both directions (42501), the blocking party can still speak, unblocking restores both, and a system line is exempt because the away reply is the product speaking
⭐A fourth pin is refused (P0001) and succeeds once one is released; muting an already-pinned conversation does not trip the cap
A colleague of the owner reads the same thread, and the same row reports viewer_side = 'merchant' to them and 'consumer' to the buyer — one function, both halves, no branching in the app
Unread counts only what the other side sent: 0 for the buyer, 1 for the merchant, on the same message
The category chip is derived through workspaces.industry_key, and the merchant gets null because all their chats are one business
The order context (QS-40182) arrives in the same round trip as the list — the composite-RPC answer to QRS-546
orders.status accepts ready; the retired collected is still refused (23514)
chat_topic_conversation_id() fails closed on junk, a bad uuid, and null
A resend with the same client_key cannot duplicate (23505); one conversation per (workspace, consumer)
Unrenderable shapes refused: whitespace-only text, an item with no item, read_at without delivered_at, and a system message carrying an author
away_reply_enabled defaults to false; last_away_sent_at starts null

⚠ A correction to the previous version of this section, which claimed Docker "did not accept connections within 550 seconds". That diagnosis was wrong. The docker CLI was not on the shell's PATH — it lives at D:\DevCache\DockerDesktop\resources\bin while the PATH carried a stale D:\Installations\Docker entry — so docker info was command-not-found, and the guard docker info || echo DOWN prints DOWN identically for a missing binary and a dead engine. CLAUDE.md already documents this exact PATH gap and I did not check it. Whether the engine was healthy during those attempts is now unknowable; what is knowable is that the check could not have told me. This is the "read an error's CATEGORY before acting on it" rule (QRS-267) recurring: a missing binary and an unready service demand opposite responses, and I spent 550 seconds on the wrong one.