لوحة الخليج — الخطة والفحص

كل سيناريو هنا مأخوذ من قرار قلته، أو من استنتاج مكتوب عليه «استنتاج» عشان تقدر ترفضه. والنتيجة أسفله ناتج تشغيل حقيقي — لا علامة كتبتها بيدي. وما لم يُفحص بعد مكتوب بوضوح، لأن جدارًا أخضر يخفي بندًا واحدًا غير مفحوص أسوأ من جدار أحمر.

المجموع

247

نجح

214

فشل

0

بالعين

1

لم يُفحص

32

آخر تشغيل: 21 أغسطس 2026، 11:50 ص · النطاق: جولة كاملة · 174 بند مبني على استنتاج — راجعها وارفض اللي ما يعجبك

0

ما ينكسر — الموقع الحالي

13 / 23

The live site is what is most at risk. Every round asks whether it still works, because a country programme that breaks Kuwait has failed no matter how well the six countries are modelled.

نجحRG18

الخلفية ·The day after a step lands, the scheduled sweeps are checked — Firestore has no triggers here, so all thirteen are polling jobs and they are the entire backend

المتوقّع: Every sweep still ran, observed by EFFECT and not by trust: no listing sits past its expiry while still live, no ad is stuck processing with photos pending, no paid order sits unreconciled — and each count is printed at zero.

الناتج: every predicate catches its own row and rejects a healthy one; production clean across 4 country drawer(s), each count printed at zero

استنتاجa broken sweep is the one regression that is invisible on the day it ships; every other row in this step looks at the web tier

نجحRG12

السجلّ ·A request is traced after each step

المتوقّع: One trace id still crosses the four chokepoints, and every API route still exports through the logging wrapper — with the two DOCUMENTED exemptions named on the run, never a bare count. From the hostname step onward, the country resolved for that request appears on the same trace.

الناتج: 24 of 27 API routes log through the wrapper; the 3 that do not each name their reason (app/api/geo/route.ts, app/api/dashboard/logs/route.ts, app/api/log/route.ts), and every event in a trace carries the country resolved from the hostname

استنتاجthe logging spine; a step that blinds the logs blinds the next step

نجحRG19

المال ·A census of money and inventory is taken immediately before a step and again immediately after

المتوقّع: Wallet and credit sums, live-ad count, active-subscription count and open-order count are identical — or every difference is traced to a named customer action inside the window. The census has to be BUILT first (the identity census counts identity, not money), and the browser suite's own spend on production is declared before the round starts.

الناتج: wallet, credits, live ads, subscriptions and open orders are bracketed per country; the suite's own spend is subtracted by name and anything else is named or fails

استنتاجthe country programme rewrites who owns what, how an amount is stored and where the wallet document lives

نجحRG20

الطلب ·A seller is mid-publish — order created, payment taken — at the exact moment a step's deploy switches over

المتوقّع: Either the listing is created, or the payment is not taken. Never a paid order with no ad behind it, and any order left in that state after a deploy is found by a sweep and named, not left for the seller to discover.

الناتج: a paid order with no ad is caught, a deleted ad is not mistaken for one, and production carries zero

استنتاجpaid publish builds the listing only AFTER payment settles, so the gap between the charge and the write is exactly where a deploy cutover lands

نجحRG21

الفراغ ·A regression round is run against a country or a market with no live ads — a brand-new country on its first day, or right after the database is wiped

المتوقّع: The gates report «no fixtures — nothing to check here» and say so on the board. They do not fail as if the site were broken, and they never fall back to an invented id.

الناتج: an empty drawer reports «لا توجد بيانات» and is excluded from the verdict; an emptiness that follows data is a finding (2 empty drawers in the last run)

استنتاجthe property gate hard-exits when its fixture query comes back empty, and the surface audit skips a route whose id it cannot fill from a live document

نجحRG22

اللوحة ·The board at /playground/countries is opened

المتوقّع: It names the commit and the timestamp of the run it is showing, and marks itself STALE the moment that run predates the deployed SHA — a stale run reads «لم يُفحص», never green.

الناتج: a run on a different tree withdraws every mark on the board — counts, badges and step pills

استنتاجrule 3 of this file: a green wall that hides one unchecked thing is worse than a red one

نجحRG07

الروابط ·Every PUBLIC live URL is requested on teswa.net — ad pages, browse, the landing pages, seller, showroom and office

المتوقّع: 200 — not a 404 and not a redirect chain. A shared ad link is someone's only way back

الناتج: 13 public pages answer 200

استنتاجlive ads carry links people already sent to each other

نجحRG08

الإعلان ·A real live listing page is opened after each step

المتوقّع: Price, place, spec row, seller and contact buttons all still render

الناتج: TW-1002176 renders price, currency, contact and JSON-LD

استنتاجthe listing page is the product

نجحRG16

النشر ·A step is deployed

المتوقّع: The SHA production is actually SERVING is resolved first — the newest Production deployment whose status is success — and it is recorded beside every live-site result. A mismatch with origin/main is a NAMED note on those rows, not a hard block: main is routinely ahead by design, and a CLI fallback deploy leaves no deployment record at all.

الناتج: serving 6fb509be — same as HEAD

قراركthe standing rule: production may be rolled back; check the deployment, not the branch

البناء3/3
نجحRG01

The whole site is typechecked after a step lands

المتوقّع: tsc --noEmit is clean — no step ships on a red typecheck

الناتج: tsc --noEmit clean

قراركthe standing verify rule in CLAUDE.md

نجحRG02

The site is linted after a step lands

المتوقّع: No NEW eslint error; any pre-existing one is NAMED, not inherited silently

الناتج: no new errors · known: components/auth/AuthModal.tsx (react-hooks/set-state-in-effect)

استنتاجa step that adds an error while fixing another has not finished

نجحRG03

Cloud Functions are typechecked after a step touches them

المتوقّع: functions tsc clean — the site and the server ship together or not at all

الناتج: functions tsc clean

استنتاجthe OTP and money rules live on both sides

نجحLAB01

المختبر ·A design round builds an «أفكار» switcher

المتوقّع: It imports components/lab/IdeasRail — the ONE implementation. Owner 2026-08-19: «every time you create option you build the tool options different than the other… Stick to one style forever.» LAB.md already forbade hand-rolling one; the rule failed because the rail was COPIED between labs and every closed exercise deleted its copy, leaving nothing to copy from. A component cannot be deleted by closing an exercise, and this scenario fails the build on a second one.

الناتج: one rail, in components/lab/IdeasRail.tsx · 35 files mention it in prose, none re-implement it

قراركhe should never have to relearn the review tool

الأسطح0/3
لم يُفحصRG04

The 48-surface audit is run against production

المتوقّع: Every surface still renders; the «not covered» list is READ, not skipped

الناتج: not built yet

قراركthe existing pnpm audit:surfaces gate

لم يُفحصRG05

The browser suite is run against production

المتوقّع: All pass — they walk the real flows a person walks. And the report names what they do NOT walk: no spec seeds a PROPERTY listing, so the property world's money and lifecycle flows (publish, pay, bump, renew, edit, archive, sold, repost) are cars-only; property identity and vocabulary are covered.

الناتج: not built yet

قراركthe existing pnpm test:browser gate

لم يُفحصRG23

The browser suite is run several times in a row against production

المتوقّع: A failure is REPRODUCED before it is called a regression, and a test that fails once and passes alone is recorded as FLAKY rather than quietly re-run until green

الناتج: not built yet

استنتاجobserved 2026-08-18: four full runs, two single failures, different tests each time (renew, then publish), neither reproducing alone or under --retries. Fifteen tests share ONE production database and one set of test accounts, so the suite races with itself

لم يُفحصRG06

السرعة ·The perf gates are run after the drawers step

المتوقّع: No page got slower — a country filter must not add a round trip

الناتج: not built yet

استنتاجthe existing perf gates; the drawers step touches every read

لم يُفحصRG09

البيع ·The sell flow is walked end to end after each step

المتوقّع: An ad can still be created and paid for — in BOTH worlds

الناتج: not built yet

استنتاجthe sell flow crosses identity, money and market at once

لم يُفحصRG10

الحساب ·A seller opens «إعلاناتي» and the business dashboard

المتوقّع: After the step, «إعلاناتي» and the business cockpit still list the same ads, the same wallet and subscription figures, and the three dates still resolve from the seller-dates helpers — driven in the BROWSER, because both pages render client-side and a server-HTML audit can neither pass nor fail them.

الناتج: not built yet

استنتاجthe identity and money steps rewrite what these pages read

لم يُفحصRG11

الأمان ·firestore.rules are re-checked after the markets and drawers steps

المتوقّع: Everything denied before is still denied — the rules default to cars TODAY

الناتج: not built yet

استنتاجthe cars default reaches into the rules file, not only the app code

لم يُفحصRG13

اللغة ·A page is opened after a copy key moves behind a market or a country

المتوقّع: No raw key name renders — a missing string shows up in review, never to a buyer

الناتج: not built yet

استنتاجworld-copy already resolves per market; a country doubles the ways a key can miss

لم يُفحصRG15

البحث ·Search is used after the markets or drawers steps

المتوقّع: Results still return; no index was renamed under a live site

الناتج: not built yet

استنتاجthe Algolia app has already gone down once and took all search with it

لم يُفحصRG17

التراجع ·A step is deployed and then the deployment is rolled back to the previous build — which has happened before, without anyone being told

المتوقّع: The site still works on the OLD build: anything the step wrote is still readable by the previous code, or the step is declared ONE-WAY before it ships and its revert procedure is written down. A step whose writes the old build cannot read goes out behind a flag, never as a straight cutover.

الناتج: not built yet

استنتاجproduction has already been silently rolled back once on this project

1

أرقام الخليج

37 / 38

A country → dial code + length table. Before this, an 8-digit Bahraini number was silently filed as Kuwaiti.

التسجيل6/6
نجحP01

A Bahraini number (8 digits, +973) is entered on Kuwait Teswa

المتوقّع: Refused as not a Kuwaiti number — NOT silently accepted as +965

الناتج: refused

قرارك«+965 only for Kuwait Teswa, other Teswa will have different country code»

نجحP02

A Kuwaiti number (8 digits) is entered on Kuwait Teswa

المتوقّع: Accepted and normalised to +965XXXXXXXX

الناتج: +96550000000

قرارك«+965 only for Kuwait Teswa»

نجحP03

A Kuwaiti number (+965) is entered on UAE Teswa

المتوقّع: Refused — a Kuwaiti number cannot register in the UAE

الناتج: refused

قرارك«Kuwaiti phone number can only able to post in Kuwait»

نجحP04

A UAE number (9 digits, +971) is entered on UAE Teswa

المتوقّع: Accepted — the length rule follows the country, not Kuwait

الناتج: +971501234567

قرارك«every country will have their own country code»

نجحP05

The same phone number registers twice on the SAME country

المتوقّع: One account — ONE PHONE = ONE ACCOUNT still holds inside a country

الناتج: «+96550000000» vs «+96550000000»

قراركthe standing identity rule, now scoped per country

نجحP06

A +965 number signs up on Kuwait Teswa and a +971 number signs up on UAE Teswa

المتوقّع: Two accounts with NOTHING linking them — separate wallets, ads, history and sessions. Teswa has no notion of «the same person behind both», and must not acquire one: THERE IS NO ACCOUNT SWITCHER. The only way to reach the other account is to sign out and sign in.

الناتج: +96550000000 → kw · +971501234567 → ae

قرارك«We will not have switch account like we had long time before. This is gone and we will not put it back. any user that might want to have multi accounts, they have to logout and login with the other account. That the only way»

الرسائل4/4
نجحP07

An OTP is requested for a number outside the site's country

المتوقّع: No SMS is sent — the allow-list is per country, not one global list

الناتج: bahraini→kuwait blocked · kuwaiti→kuwait allowed

قرارك«every country will have their own country code»

نجحP34

A client posts an over-length number straight to the OTP endpoint: 500000001234

المتوقّع: Refused as a bad number — never trimmed to 50000000 and SMSed.

الناتج: overlong refused · +9655000000012 refused on shape · real number allowed true

استنتاجthe server normaliser slices to the country length exactly as the field does, so the truncated value is a different REAL Kuwaiti number

نجحP30

The SMS allow-list is read on a country other than Kuwait

المتوقّع: Defaults to that country's dial code with no code change and no config edit

الناتج: kw +965 · ae +971

قرارك«every country will have their own country code»

نجحP38

One country's SMS volume spikes, or is pumped

المتوقّع: A DECISION: is the daily ceiling per country or shared? Today it is one global counter in one shared config document, so a pump aimed at Saudi exhausts the ceiling that lets Kuwaitis log in.

الناتج: two ceilings: the country’s own contains an attack to that country, the shared one protects the balance, both counted before the send and both tunable from /config/otp

استنتاجcountries are separated everywhere else; the OTP throttle is the one shared resource left

العرض2/3
نجحP08

A seller's number is shown on an ad, and in the dashboard

المتوقّع: Printed with ITS OWN country's dial code — never a hardcoded +965

الناتج: +965 5000 0000 · +971 50 123 4567

قرارك«every country will have their own country code»

نجحP37

A listing or a profile carries a missing, blank or junk phone

المتوقّع: No call or WhatsApp button at all — never a tel: link built from garbage. A three-digit stored value becomes «+123» today and the contact bar only checks that the value is non-empty.

الناتج: 9 stored shapes classified: junk, blank and out-of-product numbers yield no button at all («لا يوجد رقم»), and only a number belonging to a country Teswa runs becomes a tel: link

استنتاجthe empty state nobody has looked at; one seed script or one admin typo away

بالعينP09

The phone field on a country whose numbers are a different length

المتوقّع: Placeholder, max length and the error message all match that country

الناتج: needs a human eye

بالعين لأن: the wording and the spacing of the field are a design judgement, not a value

قرارك«every country will have their own country code»

التوصيل3/3
نجحP10

The real signup form on the site is given a Bahraini number

المتوقّع: Refused — and the form file itself carries no hardcoded +965 anywhere

الناتج: bahraini refused · kuwaiti accepted · form uses the table

استنتاجthe wiring: a correct helper nobody calls fixes nothing

نجحP11

The OTP function on the server is asked to send outside the site's country

المتوقّع: Refused server-side too — the browser is not the guard

الناتج: foreign refused · local +96550000000 · otp.ts uses the table

استنتاجthe wiring, server half

نجحP12

The same numbers are run through the SITE's rule and the SERVER's rule

المتوقّع: Identical answers — the two copies of the logic have not drifted apart

الناتج: 21 inputs agree on both sides

استنتاجfunctions cannot import from web/, so the only defence against drift is a check that runs both

نجحP14

التخزين ·A seller's stored number is turned into a call / WhatsApp link

المتوقّع: A foreign stored number is kept as stored — never re-badged with this site's dial code

الناتج: foreign kept as +97312345678 · local → +96550000000

استنتاجrewriting a real +973 number into +965 produces a link that dials a stranger

نجحP15

الحراسة ·Someone writes a new phone surface and types +965 into it

المتوقّع: The gate FAILS — thirteen files were cleaned and nothing else stops a fourteenth

الناتج: clean · allowed as sample data: 11 files (lib/gcc/countries.ts, lib/gcc/scenarios.ts, lib/phone.ts, lib/config.generated.ts, ../functions/src/config.generated.ts, ../functions/src/countries.ts, ../functions/src/otp.ts, test-support/fixtures.ts, components/dev/DevAccountSwitcher.tsx, components/playground/identity/data.ts, app/playground/archive/popups/recon.tsx)

استنتاجa fix that does not stay fixed is a fix that has to be made again

الإدخال6/6
نجحP16

The number is typed in Arabic-Indic digits: ٥٠٠٠٠٠٠٠

المتوقّع: Read as 50000000 — the same number, not a rejection

الناتج: +96550000000

قراركthe standing Latin-digits rule keeps the normaliser as its one exception

نجحP17

The number is pasted with spaces, dashes or brackets: «5000-0000»

المتوقّع: Accepted — punctuation is noise, not part of the number

الناتج: all 4 forms accepted

استنتاجpeople paste from their contacts, and a contact card is never clean

نجحP18

The number is pasted in 00-form: 0096550000000

المتوقّع: Accepted as +96550000000

الناتج: +96550000000

استنتاجthe 00 form is what Gulf phones store for saved international contacts

نجحP19

More digits are pasted than the country has

المتوقّع: Capped at the country length — never stored longer, and never silently truncated into a DIFFERENT real number

الناتج: field caps to «50000000» · parser refuses · genuine +96550000000

استنتاجa truncated paste that happens to be valid is the worst outcome of all

نجحP20

The field is empty, or holds only spaces

المتوقّع: Treated as incomplete — a distinct message from «this number is foreign»

الناتج: empty=incomplete spaces=incomplete short=incomplete foreign=foreign

استنتاجtwo different failures must not share one message

نجحP21

A landline is entered in a country whose mobile rule is known

المتوقّع: Refused with the mobile rule spelled out; where no rule is known, length only

الناتج: all 6 countries accept their own regulator's prefixes and refuse a landline; a Qatari 3… passes in Qatar and fails in Kuwait

استنتاجKuwait 5/6/9 is the live product rule; the other five have no verified source yet

الإدارة3/3
نجحP26

An admin edits a user's phone to a number from ANOTHER country

المتوقّع: DECISION NEEDED — refuse it (his rule), or allow it as an admin override?

الناتج: refused server-side, and the desk says why rather than «صيغة غير صحيحة»

استنتاج«a Kuwaiti phone number can only able to post in Kuwait» implies refuse; the admin route currently accepts any pasted international number

نجحP35

Any admin desk searches accounts by phone prefix, or looks one up exactly

المتوقّع: The range and the equality are built from the country table on BOTH sides — no literal +965, no literal eight-digit length. The wallet desk was fixed; its twin in the functions (the business grants lookup and its typeahead) still strips 965 and rebuilds +965.

الناتج: site desk uses the country table · server desk uses the country table

استنتاجfollows P27; the guard that would have caught it never leaves web/

نجحP27

The wallet desk searches accounts by a phone PREFIX

المتوقّع: The range query is built from this country's dial code, not a literal +965

الناتج: prefix search built from the country table

استنتاجthe prefix search is how a gift reaches the right person

نجحP28

الاختبار ·The test numbers (50000000 / 60000000 / 70000000) are used

المتوقّع: Still work, and are scoped to the country they belong to

الناتج: all 3 test numbers belong to Kuwait

قراركthe standing test-login numbers, expiring 2026-10-30

نجحP32

الدولة ·The site is BUILT for a country other than Kuwait

المتوقّع: The whole phone stack follows that country with no code edit: the field length, the placeholder, the refusal message, the OTP allow-list, and a +965 number refused at signup.

الناتج: ae → +971, 9 digits, AED/2dp, AE-7

استنتاجevery auto row proves the TABLE with an explicit country code, but every helper a page calls is bound to the site-country door, and that door has only ever answered Kuwait

نجحP33

الهوية ·A brand-new account is created after OTP

المتوقّع: The user document records the country it was created in, and the country is read from that field afterwards — never re-derived from the phone prefix.

الناتج: signup stores +96550000000 — the parser's output, never the raw field

استنتاجthe account write stores uid, phone and createdAt and nothing else; separate accounts per country makes the country an identity fact

نجحP22

التصادم ·The local digits 50000000 exist in BOTH Kuwait and Bahrain

المتوقّع: Two different stored numbers and two different accounts — no collision

الناتج: +96550000000 (kw) vs +97350000000 (bh)

استنتاجthe countries are separated, so identical local digits must not become one identity

الترحيل3/3
نجحP23

An account created before this change is loaded

المتوقّع: Its stored +965 number still resolves, still displays, still logs in

الناتج: +96550000000 → kw → «+965 5000 0000»

استنتاجno migration was run, so the old stored form must remain valid

نجحP24

A legacy number was stored WITHOUT a plus, or as bare local digits

المتوقّع: Still readable — the display falls back to the raw value rather than guessing a country

الناتج: no-plus → «+965 5000 0000» · bare local → «50000000» (not guessed)

استنتاجguessing a country for an unrecognised stored number is how +973 becomes +965

نجحP36

Every phone already stored in the live database is swept

المتوقّع: The user phone, the accounts' extra numbers and the denormalised seller phone on listings are all valid E.164 for a known country — and the sweep NAMES every row that is not.

الناتج: the sweep walks user, account and denormalised-listing phones in all six drawers, validates by the same rule the contact bar renders by, and names every row it rejects

استنتاجstep 1 shipped with no migration, only the assumption that stored numbers were already clean

نجحP25

الواتساب ·The WhatsApp button is pressed on an ad

المتوقّع: wa.me receives the full international number, no plus and no spaces

الناتج: wa.me/96550000000

استنتاجthe link format is fixed by WhatsApp, not by us

نجحP29

السجلّ ·A phone number reaches the logs

المتوقّع: Masked with NO dial code invented — a wrong prefix in a log is a false lead during an incident

الناتج: masked with no dial code invented

استنتاجredact.ts was stamping +965 onto numbers from any country

نجحP31

الأرقام الإضافية ·A seller adds a second and a third contact number

المتوقّع: Same country rule, same length, same message — the extra-number field is not a back door

الناتج: both live editors use the country rule

استنتاجthe profile and sell-flow editors each had their own copy of the Kuwait rule

نجحP13

البيانات ·A country is opened whose mobile-number ranges were never filled in

المتوقّع: mobileStarts is filled from that country's regulator FIRST — an invented range rejects real customers

الناتج: the rule refuses an empty range list and allows a filled one; all 6 countries carry first-digit ranges from their own regulator (kw:569 qa:3567 bh:3 ae:5 sa:5 om:79)

استنتاجa guessed rule is worse than no rule; the gate must name every country still empty

2

العملة لكل دولة

35 / 35

Every price and order carries its own currency and decimals. KWD/BHD/OMR are 3-decimal; SAR/QAR/AED are 2. «د.ك» is written into 196 places across 82 files today (reproduced; it was 259 across 99 before the dead labs were deleted).

نجحMO23

التحويل ·An amount is converted between minor and major units anywhere — priced, charged, verified, displayed, refunded or notified

المتوقّع: The factor is 10 to the power of that country's decimals, read from the country row. No literal 1000, no toFixed(3), no step="0.001" — including the site-side copies of the engine arithmetic in the offers and plans screens.

الناتج: ZERO hardcoded money factors — every minor↔major conversion reads 10**decimals from the country row, on both sides

استنتاجthe same conversion is hand-written in at least fifteen places across web and functions, and the subscription module's «no floats leave here» rule covers only subscriptions

العرض3/3
نجحMO28

Someone sets out to replace the hardcoded «د.ك» across the site

المتوقّع: There is ONE money renderer to call and it accepts the units prices are actually stored in. The current helper takes MINOR units while ad prices are stored as bare major-unit integers, so today no helper can render an ad price with a country's currency.

الناتج: one module answers both units: an ad price «41,750 د.ك», the same integer as a charge «41.750 د.ك», and a Saudi ad price «41,750 ر.س»

استنتاجMO01 and MO02 say what may not be written; nothing says what replaces it

نجحMO01

An ad price is printed anywhere on the site

المتوقّع: The currency comes from the ad's country — never the literal «د.ك»

الناتج: every amount derives its symbol from the row; zero hand-written symbols anywhere in app, components, lib or functions

قرارك«Same for currency»

نجحM02

Prices in a 2-decimal country (SAR/QAR/AED) and a 3-decimal one (KWD/BHD/OMR)

المتوقّع: Each shows and stores its own currency and decimals — the same amount prints with three decimals in Kuwait and two in Saudi.

الناتج: 12.500 د.ك · 1,250.00 ر.س

قرارك«Same for currency»

الأرقام2/2
نجحMO21

Any amount is rendered in Arabic RTL

المتوقّع: Latin digits, the currency on the correct side, the whole amount in one bidi isolate

الناتج: «12.500 د.ك» — Latin digits, symbol after the number, one isolate

بالعين لأن: bidi is judged by eye — a screenshot, not a value

قراركthe standing Latin-digits rule; amounts have already broken bidi once

نجحMO33

A number formatter is added anywhere in the site or the functions

المتوقّع: The gate FAILS on any locale-formatting call made with an Arabic locale or a non-latn numbering system — Latin digits are the standing rule and only the input normaliser is exempt.

الناتج: 25 Arabic date formats, every one carrying -u-nu-latn — Latin digits everywhere, as ruled

قراركthe standing Latin-digits ruling; MO21 judges the rendering by eye, this keeps the regression out

نجحMO15

البحث ·A price is indexed for search

المتوقّع: Minor units + currency in the record, so sorting is numeric and comparable within a country

الناتج: every index record carries a numeric price, its currency and its country — both world builders

استنتاجsorting mixed-currency records by a raw number is silently wrong

الإدارة3/3
نجحMO11

The dashboard shows revenue for a country

المتوقّع: One currency per view; two countries are NEVER summed into one number

الناتج: revenue is read from ONE country’s drawer and converted with that country’s factor — no cross-drawer sum exists

استنتاجsumming KWD and SAR produces a number that means nothing and looks authoritative

نجحMO12

The cross-country console shows money

المتوقّع: Per-country figures side by side, each labelled — or no total at all

الناتج: each country’s figures stand on their own row with their own currency; nothing is summed across them

استنتاجfollows MO11; the console is the one place tempted to add them up

نجحMO25

A money total is rendered on a page that is not scoped to one country

المتوقّع: No cross-country total is invented: the figures stand side by side, each labelled with its own currency, or there is no total at all.

الناتج: the one cross-country page keeps every figure on its own country’s row and invents no total

استنتاجsplit out of M02, whose auto check only ever proved the decimals

التخزين3/3
نجحMO03

An amount TESWA CHARGES is stored — an order, a package, a tier, a bump pack, an extend option

المتوقّع: An integer in that country's minor units with its currency code beside it — never a float, never a bare number.

الناتج: every charge is an integer of minor units carrying its currency code and its decimals

استنتاجa 3-decimal amount kept as a float is a rounding bug waiting for a refund

نجحMO27

The stored money field names are read on the day a second country opens

المتوقّع: A DECISION: the currency is baked into the Firestore field names (priceKwd, monthlyPriceKwd, priceFils, amountFils). Rename them with a migration of the money catalogue and every existing order, or keep the names and reinterpret them per country — decided before data exists in country number two.

الناتج: decided and written down: the names stay and are read per country. 16 stored fields carry a currency word (baseline 16), ratcheted — a new one fails this.

استنتاجthese are document fields in the lookups catalogue, the tier docs and every order row, not variable names

نجحMO26

An AD's price is stored

المتوقّع: It carries its country's currency wherever it is stored, read, indexed or exported. THE UNIT IS DECIDED (owner 2026-08-18): whole units in every country — no decimals on any ad price, ever, including SAR/AED/QAR — with the currency stored beside the number. Car and property prices are five and six figures in every Gulf currency, so decimals buy nothing and cost a class of rounding bugs.

الناتج: all 5 writers stamp it, and firestore.rules requires an int price + a currency on create

قراركsplit out of MO03: charged amounts are already integer minor units with a currency, ad prices are bare major-unit integers with none

نجحMO04

الحساب ·A 3-decimal amount is halved, or a percentage discount is applied

المتوقّع: Rounded in minor units by an explicit rule — never left to floating point

الناتج: 7 explicit roundings in minor units — every proration and discount states its direction

استنتاجKWD 0.0005 does not exist, and the rounding direction must be decided once

الباقات2/2
نجحMO07

Subscription tiers are read in Qatar

المتوقّع: Qatar's own prices — NOT 15 KWD converted, and not 15 QAR by coincidence of the number

الناتج: each country reads its own «countries/{code}/config/business» — no conversion, no shared document

استنتاجthe tier catalogue is priced per market today (monthlyPriceKwd); it becomes per country

نجحMO08

A bump pack or an extend package is priced in a 2-decimal country

المتوقّع: Its own price and decimals — the money library is per country

الناتج: all 6 money lists resolve per country; Qatar prices in 2 decimals and Kuwait in 3

استنتاجthe «الأساس» core library is per market today; country is the level above

المحفظة2/2
نجحMO10

Free-ad credits (adCredits) are shown beside money

المتوقّع: Clearly NOT currency — coins are a count, not an amount

الناتج: credits render as a count with a noun («إعلان مجاني») and are never passed through a money renderer

قراركthe free-ads wallet is «real coins», a separate unit from money

نجحMO09

A money-valued balance is shown to a seller — a subscription proration credit, a refund credit, any future stored balance

المتوقّع: In their country's currency; a Kuwait balance never appears on another country's site

الناتج: balances are read from the seller’s own country drawer and rendered with that country’s currency, on screen and in notifications

قراركseparate accounts per country ⇒ separate wallets

نجحMO14

التصفّح ·Price filters and price sorting are used on browse

المتوقّع: Ranges and steps follow the country's currency, not Kuwaiti dinars

الناتج: the filter names the country’s currency, takes whole amounts, and sorts inside one country’s drawer

استنتاجa KWD-shaped price ladder is meaningless in SAR

نجحMO22

الاشتراك ·A dealer downgrades or renews in a 2-decimal country

المتوقّع: The «الانتقال» wording and the proration arithmetic both work in that currency

الناتج: the «الانتقال» wording holds and both halves of the breakdown render in the dealer’s own currency and decimals

قراركthe subscription wording is locked; only the money underneath changes

نجحMO17

الفاتورة ·A receipt or an invoice is produced

المتوقّع: The currency named in full (KWD/SAR), not only a symbol, and the country stated

الناتج: 2 receipt surfaces name the currency in full AND the country, both read from the order's frozen values

استنتاجد.ب and د.ك are one letter apart on a document someone may file for tax

نجحMO20

التصدير ·A table of money is exported or copied

المتوقّع: The currency travels with the number

الناتج: 3 export path(s); every one carrying money puts the currency on the ROW, so two countries' sheets cannot merge into one number

استنتاجa column of bare numbers from two countries is a spreadsheet that lies

نجحMO30

الفراغ ·A country is activated before its packages, extend options and bump packs have been priced

المتوقّع: The sell flow and the packages screen say the country is not open for paid publishing yet — they do not render an empty list, and they never fall through to a free publish.

الناتج: price lists live at countries/{code}/money/{name}, cannot be read without a country, and «5» becomes minor units by THIS currency’s decimals

استنتاجthe packages loader filters on active and returns an empty list with no caller checking for emptiness; the server throws free_item when the amount is zero

نجحMO02

الحراسة ·Someone writes a new price surface and types «د.ك» into it

المتوقّع: The gate FAILS, exactly as it does for +965

الناتج: baseline 12 files still carry a currency literal (30 cleared since the baseline was written — rerun with --money-baseline). NOT passing, RATCHETED: no NEW file may acquire one.

استنتاج196 occurrences across 82 files today, reproduced; without a guard the 215th arrives next month

الدفع3/3
نجحMO18

The payment gateway is called

المتوقّع: The currency is passed explicitly and the SIMULATOR accepts every currency in the table — it is ours, so it does. Real Tap's merchant accounts and per-market minimums are a GO-LIVE question, filed with the gateway swap and deliberately not a blocker now.

الناتج: the simulator takes any currency in the table, asserts it at the write, and floors per currency

قرارك«we will deal with Tap later. Let's just get the project done for now»

نجحMO31

A dealer in Saudi pays for a subscription

المتوقّع: The whole chain carries SAR: 4000 halalas → «40.00» on the charge and in the signed digest → 4000 back at settle, exactly. The gateway floor is a tenth of a whole unit in EACH currency, not a flat 100 (which would have meant 0.100 KWD but 1.00 SAR — the same integer, ten times the money).

الناتج: 4 currencies round-trip exactly · gateway floor per currency: KWD 100, SAR 10

قرارك«we made fake payment gateway, set it/pretend that it accept multiple currencies. we will deal with Tap later»

نجحMO24

A payment is captured in a country whose currency is not KWD and the webhook settles the order

المتوقّع: The charge amount and currency are compared against the ORDER's own stored currency and its own minor-unit factor, never against a literal — and a genuine mismatch still parks the order as held.

الناتج: KWD 1.500↔1500 true · SAR 40.00↔4000 true · the old ×1000 rule would have held that Saudi order: true

استنتاجMO18 covers the outbound call; nothing covers the verification on the way back, which is where the money is already gone

الطلب2/2
نجحMO05

An order is created and paid

المتوقّع: The order records the currency AT THE TIME OF PURCHASE and never re-reads it later — so a later change to the country's currency cannot rewrite an old receipt

الناتج: all three order writers stamp country + currency + decimals

قرارك«This is for sure correct: Orders already freeze their currency at purchase (MO05), so old receipts stay right regardless.»

نجحMO06

An old KWD order is opened after a second country goes live

المتوقّع: Still renders as KWD with 3 decimals

الناتج: parseOrder carries the order's own currency into every money surface

استنتاجa receipt is a record, not a view of today's config

نجحMO16

المشاركة ·An ad's OG card and meta description are generated

المتوقّع: The right currency for that ad's country

الناتج: OG card uses the ad's ISO code · JSON-LD uses the ad's currency

استنتاجthe OG card is the first thing a buyer sees in WhatsApp

نجحMO19

الاسترجاع ·A refund or a credit is issued against an order

المتوقّع: Same currency and same decimals as the original order

الناتج: the refund path derives its factor from the order and alerts on a currency mismatch

استنتاجfollows MO05

نجحMO29

التسعير الذكي ·«السعر المقترح» and the market range bar

المتوقّع: GONE. Owner 2026-08-18: «Delete and retire السعر المقترح full from Teswa. We will never have it.» PriceStep.tsx, price-index.ts, price-insight.ts, the functions priceIndex sweep and the /price_index collection are all deleted — not disabled, not parked.

الناتج: all 5 files deleted and the hourly sweep removed

قرارك«Delete and retire السعر المقترح full from Teswa. We will never have it»

نجحMO32

المجاني ·A package, an extend option or a bump pack is priced at zero in any country

المتوقّع: «مجاني» — never «0.000 د.ك», never «0.00 ر.س», and it never reaches the gateway.

الناتج: 0 → «مجاني» · 1500 → «1.500 د.ك»

استنتاجthe lookups editor special-cases zero into «مجاني» while the server throws free_item and other surfaces would run a zero straight through the formatter

نجحMO13

البيع ·A seller types a price with a decimal point — «1250.00» — in the cars sell flow, the property sell flow, or the edit page

المتوقّع: REFUSED and told why, not silently read as 125000. Ad prices are whole units in every country, so a decimal point is a typo. The three LIVE inputs are the cars shell, the property BigNum and the account edit page — «the price step» named earlier is an unreachable file.

الناتج: decimals refused true · «41,750» survives true · Arabic digits true · all 3 live inputs wired

قراركa Saudi seller typing 1250.00 must not be read as 1.250

نجحMO34

الوحدة ·The SAME stored integer 41750 is rendered as an ad price and as a charged amount

المتوقّع: Two different amounts, a thousand apart: «41,750 د.ك» for the ad and «41.750 د.ك» for the charge. Teswa stores ad prices in WHOLE units and charges in MINOR units, and both are plausible Kuwaiti money — so the units live in the TYPES and the wrong one cannot compile

الناتج: ad «41,750 د.ك» · charge «41.750 د.ك» · same integer in SAR «417.50 ر.س»

استنتاجa live ad reads price:41750 meaning 41,750 د.ك while priceFils:41750 means 41.750 د.ك — rendering an ad through the charge formatter advertises a car at forty-one dinars

3

الأسواق قائمة لا كود

19 / 21

A country can run a different set of markets. Today the two markets are a hardcoded pair across 260 places.

البحث3/3
نجحMK09

Search is performed inside a market

المتوقّع: The index resolver takes (country, market, sort) — no ALGOLIA_INDEX constants left

الناتج: 10 (market, sort) pairs resolve from the registry: cars/new=listings · cars/cheap=listings_price_asc · cars/exp=listings_price_desc…, and an unoffered sort falls back to the primary index

قرارك«Name the index by country from day one. Agree, added to the plan»

نجحMK17

A new country-and-market pair is activated and a buyer searches in it

المتوقّع: Its search index and every sort replica were created and populated BEFORE activation; a resolver pointing at a missing index is a hard, named failure, never an empty result set.

الناتج: one index per market (listings, listings_re) carrying every country as a facet — opening a country creates no index, so there is none to forget; replicas are derived from the market's declared sorts

استنتاجMK09 fixes the resolver and nothing provisions the indices; cars alone runs six sort replicas today

نجحMK18

A search is filtered by country

المتوقّع: `country` is DECLARED as a facet on every searched index — both market indexes AND the property sort replicas, which are standard copies carrying their own settings rather than virtual ones inheriting the primary's. Algolia ERRORS on a filter against an undeclared attribute, so the declaration must ship before the first query that uses it, and a half-declared set means country filtering works on «الأحدث» and dies on «الأرخص».

الناتج: country declared on both market indexes and pushed to the property sort replicas · verified live: 10 indexes accept country:kw (30 cars · 14 property) and country:ae (0) without error

استنتاجthe deploy-order trap this codebase already hit once with `category`

الأسواق3/3
نجحMK15

A third market is added and someone counts what a person actually has to author

المتوقّع: A data change plus a NAMED, listed set of authoring steps — the country's market row, its copy row, its lookups vertical, its search indices — and the gate PRINTS that list on every run.

الناتج: adding a market is data plus 6 authoring steps — (1) the market row in shared/config (17 fields: publicPrefix, extraPrefixes, icon, adminSlug, venueSlug, venueProfileBase, configDocSuffix, packagesList, lookupsVertical, searchIndex, directoryMinStock, nameAr, nameEn, routes, sorts, ofNameAr, venuesAr) · (2) its copy row in lib/world-copy.ts · (3) its lookups vertical + lists in lib/lookups-verticals.ts · (4) its search index and the replicas for the sorts it declares (7/3 today) · (5) its money lists in MONEY_LISTS, priced per country before the grid will open it · (6) the country’s `markets` array — opened per country from the master grid

استنتاجfollows MK01: «no code change» is the target, and the printed list is what proves it

نجحMK01

A THIRD market is added to a country

المتوقّع: No code change — nav, landing, sell, dashboard and search all pick it up from the list

الناتج: 2 markets, 12 explicit namings each · an unknown market throws

قرارك«we will increase number of markets… several» over three years

نجحM03

A country is configured with property only

المتوقّع: Its landing, nav, sell flow and dashboard offer property only — cars is ABSENT, not disabled

الناتج: kw runs [cars, realestate] in that order · qa runs [none yet] — a country with no markets is a legal state

قرارك«it is very possible that country have more or less markets than the other»

نجحMK11

المال ·Money settings are opened for a country running three markets

المتوقّع: Each market has its own paid catalogue; the free layer stays ONE across markets

الناتج: 2 paid catalogues, one per market and each per country (packages, rePackages); ONE free layer across all of them — one wallet field, one policy document

قراركthe standing rule: PAID is split per world, the FREE layer is one

الإطلاق2/2
نجحMK16

A market that already has live ads and paying dealers is switched off in one country

المتوقّع: Buyers stop seeing it, the ads are not deleted, and whatever was decided for a deactivated country applies here too — the same clocks, the same money answer, and re-activation restores the exact remaining days.

الناتج: closing a market removes it from the country’s list and nothing else — the ads stay, the world stops being reachable, and the last open market cannot be closed on a live country

استنتاجMK08 covers only the not-yet-activated direction; this repo already refused to apply a switched-off default retroactively because «that is not a migration, it is an outage»

نجحMK08

A market is created but not yet activated in a country

المتوقّع: Admins see it, buyers do not — the same door as country activation

الناتج: 2 markets in the registry, 0 running nowhere yet; opening one is refused until that country has priced it

استنتاجstaged rollout is the whole point of one frontend and many datasets

الحراسة2/2
نجحMK02

Someone writes `world === 'cars' ? … : …` in new code

المتوقّع: The gate FAILS — a two-way branch is a hardcoded market list wearing a disguise

الناتج: the two-market ternary is ratcheted by CA05 at 47 — it may only fall

استنتاج260 literal 'cars' sites today; the pair is the pattern that must stop spreading

نجحMK21

A third market is added to the registry

المتوقّع: Only DATA is missing — its Arabic words, its dashboard label — and every code path already handles it. With exactly two markets `'cars' | 'realestate'` and MarketId are the SAME TYPE, so a hand-written pair compiles perfectly and stays wrong until the day a third arrives. Adding one is the only thing that tells them apart.

الناتج: a third market produces only «this table needs a row», and no hand-written pair remains

استنتاجthe first probe found four private copies of the union and a two-key Record duplicating a field the market table already carried

نجحMK06

الأمان ·firestore.rules are evaluated for a market

المتوقّع: Rules are market-aware by data, not by an enumerated pair

الناتج: the rules and the registry name the same 2 markets (cars, realestate)

استنتاجthe rules file names cars explicitly today

نجحMK05

المفردات ·Lookups are read for a market

المتوقّع: Nested under that market; a new market gets its own vocabulary without touching the others

الناتج: every list is routed to a market or to «shared», and an unrouted name throws where someone can see it

قراركlookups are already nested by vertical — this extends it

نجحMK13

الإحصاء ·Daily stats are aggregated

المتوقّع: Stamped with market AND country; a new market needs no new counter field

الناتج: 4 counters stamp the market from the ad’s own row, and none counts an ad whose market is unknown

استنتاجdailyStats are already category-stamped

نجحMK19

الهوية ·A market declares which icon represents it

المتوقّع: The icon EXISTS. `icon` is a string in shared config and every call site writes `m.icon as IconName` — a cast is a promise the compiler is told to believe, not a check. realestate promised «building»; the registry only has «reBuilding», so the property door drew nothing at all, silently, on every option of a design round.

الناتج: every market's icon exists: cars→car · realestate→reBuilding

استنتاجa blank where a market should be is invisible to tsc, eslint and the build

نجحMK04

اللغة ·Copy is resolved for a market

المتوقّع: Every string comes from the market's copy row, and a key missing for ANY market in the country's list FAILS THE GATE BY NAME — «caught in review» is not a check.

الناتج: 5 surfaces derive their markets — none enumerates a pair

استنتاجworld-copy already exists — the risk is a key only Kuwait's two markets have

نجحMK07

الترتيب ·The markets are listed to a buyer

المتوقّع: In an order the admin sets — not alphabetical, not insertion order

الناتج: kw order: cars → realestate

استنتاجthe seller-facing order rule already exists for money items

نجحMK10

الإدارة ·The dashboard's market tabs are rendered

المتوقّع: Built from the country's market list; a one-market country shows no tab strip at all

الناتج: the switcher hides on fewer than TWO markets — a count, not a market name

استنتاجa tab strip with one tab is a bug the reader has to interpret

نجحMK03

الروابط ·A URL names a market the country does not run

المتوقّع: A clean 404, or a redirect to that country's landing — never a half-rendered page

الناتج: 15 paths resolve exactly, on segment boundaries · «/» owned by no market

استنتاجa market that does not exist here must fail like a missing page, not like a bug

لم يُفحصMK12

الملف ·A seller profile is opened in a country running three markets

المتوقّع: Their ads group by market; a market they never used renders no empty section

الناتج: not built yet

استنتاجshowrooms and offices are already the same component with a world prop

لم يُفحصMK14

المفضلة ·A buyer's favourites and saved searches span two markets

المتوقّع: Grouped by market, still resolvable when a market is later deactivated, and each one carries its market EXPLICITLY — a cars saved search identifies itself today by the market field being ABSENT. Stamping it rewrites the dedupe key, the alert payload and the «new since you looked» markers together, or dedupe breaks and every unread badge resets.

الناتج: not built yet

استنتاجthe buyer tools box holds all three lists together

4

السيارات سوق مثل غيره

20 / 32

Cars stops being the site root and the default value. 24 `?? cars` fallbacks decide what happens when the world is unknown — including the posting gate and firestore.rules.

العناوين2/3
نجحCA21

The world-prefix guard is re-run after the routes move

المتوقّع: Still passes: every top-level route root either belongs to a market or is declared neutral with a reason. It does NOT check its own list — a prefix left pointing at a route that MOVED is invisible to it, which is CA22.

الناتج: no uncovered route directory (stale-entry direction is CA22, not checked)

استنتاجthat guard already shipped with the bug it guards against («THE LIST IS THE BUG»)

نجحCA22

The prefix lists are checked in the other direction after the routes move

المتوقّع: No DEAD prefix survives: every string in the cars and property prefix lists still resolves to a route that exists — matched by SEGMENT and walked recursively, because after the move the prefixes are nested and the current guard reads only the top level of the route tree.

الناتج: no internal link addresses a retired route; every one is built from the registry (11 route names checked in both markets)

استنتاجsplits out the half CA21 was asserting and never running

لم يُفحصCA25

Cars is given its own path prefix and the route tree is read

المتوقّع: The collision is named and decided BEFORE any route moves: /cars/[make] and /cars/[make]/[model] already own /cars/*, so a market home at /cars and a browse at /cars/browse land on top of /cars/toyota and /cars/toyota/camry. Property has the mirror defect — /realestate is a prefix but its landings live outside it at /realestate-in and /realestate-type. A static check reads the route tree against the proposed prefix map and fails on any collision, or any market landing left outside its own prefix.

الناتج: not built yet

استنتاجthe first thing the build hits, and nothing in the step mentions it

الروابط2/3
نجحCA03

Every existing cars URL is requested: browse, filters, listing, showroom, compare

المتوقّع: All resolve, and search engines are told where they moved

الناتج: 8 landing families + the ad breadcrumb derive every address from the registry

استنتاجfollows CA02

نجحMK20

A link to a retired address is opened — a WhatsApp thread, a bookmark, a Google result

المتوقّع: A PERMANENT (308) redirect to the new address, never a 404 and never a temporary redirect. Owner 2026-08-19 chose the nesting shape knowing it moves 13 of 16 addresses, including the make and model pages, which are the strongest pages Teswa has in search — a 302 there asks Google to keep the OLD url as canonical, which is how a move becomes a loss.

الناتج: 10 retired addresses redirect, all permanent (308)

استنتاجevery retired address is live somewhere out of our control

لم يُفحصCA02

A cars link shared in WhatsApp months ago is opened — /browse and its filters, /cars/toyota, /cars/toyota/camry, /cars-in/hawally, /cars-body/suv, /brands, /showrooms, /showroom/<uid>, /listings/TW-1234

المتوقّع: Every one still lands on cars — a permanent redirect, never a 404. The bare «/» is the one exception: it becomes the country landing (CA01), and the edge preference redirect on it moves or is deleted by decision, not by silence.

الناتج: not built yet

استنتاجlive ads carry links people already sent each other; breaking them is invisible to us and total for them

الأرشفة2/2
نجحCA12

The sitemap and robots are generated

المتوقّع: Both markets are listed on equal footing and the old root is no longer advertised as the cars page; the static entries are machine-checked — «/» listed as the country landing, and each market root at the same priority.

الناتج: 2 market roots listed from the registry, all at priority 0.9

استنتاجfollows CA01 and CA02

نجحCA23

robots.ts is read after the routes move

المتوقّع: Its disallow list moves with the routes: «/browse?» is blocked today to stop bots crawling filter permutations and defeating the cache, and that protection dies silently the moment browse goes under the cars prefix.

الناتج: every market's browse filters are disallowed by its CURRENT address (/cars/browse?, /realestate/browse?), and no guard points at a retired one

استنتاجCA12 names robots.ts and tests nothing in it

نجحCA19

الخطأ ·A 404 or an error page is shown

المتوقّع: It offers the country's markets — it does not offer «go to cars»

الناتج: the 404 offers every market the country runs, and «الرئيسية» is the country door

استنتاجfollows CA01

الإعلان1/2
نجحCA27

A CARS ad's listing page is opened by a visitor whose saved preference is property

المتوقّع: The page declares its own market from the document's own category. The listing route is deliberately NEUTRAL and only the property detail declares itself today, so a cars ad declares nothing and rides the cars default — both branches must declare.

الناتج: both branches declare the ad’s market from the document; the route stays neutral

استنتاجCA13 covers the OG card, not the page's own chrome

لم يُفحصCA09

An old listing document has no world field

المتوقّع: BACKFILLED from its data before the step ships — not resolved at read time

الناتج: not built yet

استنتاجread-time guessing is the same bug with extra steps

الأمان2/2
نجحCA28

A business account whose market was never recorded posts an ad

المتوقّع: Refused, not defaulted — the rules and the server both fill in cars on the account's behalf today.

الناتج: a business with no market is refused by both the rules and the server

استنتاجCA06 covers the LISTING with no market; nobody covers the ACCOUNT with no market

نجحCA06

firestore.rules evaluate a write with no world on it

المتوقّع: Denied — the rules must not fill in cars on the writer's behalf

الناتج: a write with no market is denied, and the rules fill nothing in

استنتاجthe rules default to cars today, so a malformed write becomes a valid cars write

نجحCA14

الحفظ ·A saved search or a favourite created under the old root is opened

المتوقّع: Still resolves to the same results in cars

الناتج: the saved-search alert builds its link from the market row’s routes.browse

استنتاجsaved searches store a URL shape that is about to change

نجحCA01

الجذر ·A visitor opens teswa.net/

المتوقّع: A Teswa landing listing that country's markets — NOT the cars HOME that «/» serves today. And the edge redirect that sends «/» to the property home for a returning property visitor moves with the market or is deleted deliberately: after the demotion it silently reinstates a per-market root for the one visitor most likely to want the country landing.

الناتج: the door renders at «/», the site default names the country’s markets, and both cars pages carry their own line

قرارك«cars is just a market like the real estate»

النشر1/2
نجحCA07

The posting gate runs on an ad whose market is not set

المتوقّع: Refused — the gate decides quota and price, and guessing the market guesses the money

الناتج: both posting doors refuse an ad whose market is not set

استنتاجthe posting gate is one of the 24 fallbacks

لم يُفحصCA29

The route move is deployed to production

المتوقّع: The redirects ship in the SAME commit as the moved routes, and the edge caches are purged and re-verified — the landing is ISR and the sitemap is cached for an hour, and the standing purge tool only covers listings.

الناتج: not built yet

استنتاجthe standing rule that the site lies for ten minutes after a write unless purged; this is a far bigger write than a listing edit

الافتراضي1/2
نجحCA26

A page is server-rendered after the demotion and the world resolver is asked about «/»

المتوقّع: It no longer answers cars, and the provider's SERVER snapshot is no longer the literal cars world. Verified by calling the resolver AND by reading the server HTML of a property URL for the cars header strings.

الناتج: 6 paths resolve exactly, «/» owned by no market, and the provider's default is the country's first market

استنتاجthe world resolves on the CLIENT with a cars server snapshot, which the surface audit structurally cannot see and says so in its own docstring

لم يُفحصCA04

Anything in the system reads a world that is missing or unknown

المتوقّع: It FAILS or asks — it must not silently become cars

الناتج: not built yet

استنتاجthe 24 `?? cars` fallbacks are the actual bug; property ads saying «0 كم» came from exactly this

البحث2/2
نجحCA24

The site and the functions are both asked which index a market-and-sort pair resolves to

المتوقّع: Identical answers on both sides — the same assertion P12 already runs for the phone rules, because functions cannot import from web/.

الناتج: both sides resolve the same index per market (cars=listings,realestate=listings_re) — a URL change never touched an index name

استنتاجthe index names and their sort replicas are hand-copied into web/lib/algolia.ts and functions/src/algolia.ts

نجحCA16

Search runs after the demotion

المتوقّع: The cars index keeps its name and its records — a URL change must not touch the index

الناتج: the cars index is still «listings» — the routes moved and the index did not, which is the whole point of naming it in the registry

استنتاجa rename under a live site is how all search dies at once

نجحCA11

التنقّل ·The nav and the breadcrumb are rendered inside cars

المتوقّع: Cars appears at the same level as property — no visual seniority

الناتج: the switcher renders the country’s markets in ITS order and the brand link follows whichever market you are standing in — cars is named nowhere

بالعين لأن: equal footing is a design judgement — made by looking at the two side by side

قرارك«cars is just a market like the real estate»

نجحCA18

السجلّ ·A request is logged after the demotion

المتوقّع: The market is stamped from the request, never defaulted

الناتج: a log event carries the market when the request had one and omits it otherwise — no event defaults to cars

استنتاجfollows CA04, applied to the logging spine

نجحCA17

التطبيق ·A caller that is not the browser reaches the backend after the demotion — the thirteen scheduled sweeps and the payment webhook

المتوقّع: Each reads the ad's market from the document itself and never assumes cars; any break is named and scheduled.

الناتج: 5 scheduled sweeps in index.ts read the market from each row and collect the unrecognised ones by id; the webhook resolves it from the order

استنتاج«the site leads, the app is outdated», but a silent break is still a break

الحراسة1/2
نجحCA05

Someone writes a new cars default — `?? 'cars'`, a ternary collapsing to cars, or `= 'cars'` as a parameter default

المتوقّع: The gate FAILS on any INCREASE. The ternary is the one that matters and nobody had counted it: it does not fill in a blank, it CONVERTS a third market's real value into cars, and there are more of them than the `??` form.

الناتج: 51 cars defaults — fills-a-blank 0 · converts-a-value 0 · hardcoded-default 0 · means-not-property 5 · branches-on-property 46 (fell: branches-on-property 47→46 — rerun with --market-baseline). RATCHETED, not passing.

استنتاجthe 24 `?? cars` fallbacks are the bug; the ternaries are the same bug doing something worse

لم يُفحصCA30

The audit and gate scripts are run to prove the demotion worked

المتوقّع: They were re-pointed at the new URLs first — the world audit picks its world with a cars fallback and requests routes by hardcoded path, and the surface audit reads each document's category with a cars fallback.

الناتج: not built yet

استنتاجthe step's own verification tooling is inside the blast radius

لم يُفحصCA08

البيع ·A seller starts «أضف إعلان» from the landing

المتوقّع: The first question is which market — no market is pre-selected for them

الناتج: not built yet

قرارك«cars is just a market like the real estate»

لم يُفحصCA10

الهوية ·The header logo is clicked from anywhere on the site

المتوقّع: The country landing, not the cars browse

الناتج: not built yet

استنتاجfollows CA01

لم يُفحصCA13

المشاركة ·A cars ad is shared and its OG card is generated

المتوقّع: Unchanged — an ad's identity does not depend on cars being the root

الناتج: not built yet

استنتاجthe OG pipeline reads the world today

لم يُفحصCA15

الإدارة ·The dashboard is opened with no world cookie set

المتوقّع: It ASKS which market — it does not open cars

الناتج: not built yet

استنتاجfollows CA04, applied to the admin surface

لم يُفحصCA20

السرعة ·The new landing page is measured

المتوقّع: No slower than the cars browse it replaced — the root is the most-hit page on the site

الناتج: not built yet

استنتاجthe perf programme; the root page sets the first impression

لم يُفحصCA31

الفراغ ·A market in the country has zero live ads when the new landing is rendered

المتوقّع: The country landing still shows that market as a real, enterable choice, and the market's own home renders an honest empty state — not a blank page.

الناتج: not built yet

استنتاجthe landing builds its taxonomy from per-make and per-body counts, and zero-count entries hide themselves

5

الدول قائمة

34 / 37

The registry row + the page to create, rename, activate and deactivate a country.

الإعدادات5/5
نجحCO35

An admin closes a market for one country in the master grid

المتوقّع: That country’s site stops offering it — the switcher, the country door, the 404 suggestions and the page title all read the country’s OWN row, never the build constant. A country nobody has configured falls back to the build; a country configured shut stays shut, and the two are not the same value

الناتج: every switcher, the country door, the 404 suggestions and the page title read the country’s own row; absent falls back to the build and an empty list stays empty

قرارك«turning the market on and of for the coutires is done in the master catagories»

نجحCO36

A Qatari admin opens the car models list and tries to rename one

المتوقّع: Refused server-side, with a sentence saying it belongs to the master category — the shared vocabulary is edited in ONE place, because renaming «شقة» in Qatar renames it in Bahrain and deleting a model leaves every country’s ads pointing at nothing. Their own prices and packages still save normally

الناتج: 7 shared vocabulary lists are master-only and 6 country lists stay editable; the refusal is server-side and reaches the screen

قرارك«they can not add or edit the types of the properties cars model, these will be done only in the master category»

نجحCO32

A Qatari admin opens the packages page and changes a price

المتوقّع: The number changes in Qatar and nowhere else — every business setting (packages, prices, tiers, the free-ad policy, offers, limits, the banner, legal, the gateway) is that country’s own document, and the writer lands where the reader looks

الناتج: 11 documents belong to the country, 6 to the platform; identical on both sides, nothing addresses /config by hand, and an undeclared document throws

قرارك«countries should be totally separated. all the configuration separated»

نجحCO33

A setting that must NOT differ between countries is looked for

المتوقّع: Feature flags, notification templates and the OTP allow-list stay platform-wide — a login bypass that works in one country and not another is worse than no bypass, and a half-built feature on in one country is two products to support

الناتج: 5 engineering switches stay platform-wide, 10 business decisions belong to the country, and the reason is written beside each

استنتاجderived from the same ruling: «all the configuration» means the business decisions, not the engineering switches

نجحCO34

A new config document is added by someone who has not read this

المتوقّع: It has no scope and THROWS until classified — no default, because defaulting to platform lets one country edit another’s prices and defaulting to country strands a feature flag nobody copied

الناتج: an undeclared document throws on both sides, and the message asks the one question that decides it

استنتاجderived

الأماكن5/6
لم يُفحصCO08

Places are entered for a country with three levels (Saudi, UAE, Oman)

المتوقّع: The place tree is CAPPED at 3, never FIXED at 3 — depth varies per city and per market inside one country (Riyadh 3, Khobar 2, Kuwait 2), so depth is a property of the branch, not a per-country setting, and there is no fork between a two-level and a three-level country.

الناتج: the model is right — Riyadh runs three levels and Khobar two in one tree, and neither invents a level — but nothing on the site reads this tree yet — geography is still the flat areas/districts lists

قرارك«we are not going to have البرج, the max we can go is الحي»

نجحCO09

A place is searched in English in an Arabic-slug country

المتوقّع: «Dubai Marina» resolves through the Latin aliases

الناتج: Latin aliases, partial spellings and the Arabic name all reach the same row; a switched-off place is kept for labels and hidden from search

قرارك«Slugs are ARABIC in every country» + the agreed alias mitigation

نجحCO20

Two different places inside one country carry the same Arabic name — عين خالد sits under two Qatari cities, and منطقة الرياض is not مدينة الرياض

المتوقّع: The stored id and the URL slot disambiguate: a place URL resolves to exactly one place or 404s — it never merges two places' ads under one Arabic slug.

الناتج: the duplicate name is qualified by its parent («عين-خالد» vs «عين-خالد-الريان»), each URL resolves to exactly one place, and an unknown one resolves to null

استنتاجTeswa's location model rests on a Kuwaiti fact: Kuwaiti area names are unique nationwide, so a bare name is a sufficient key

نجحCO22

A country's places are stored

المتوقّع: A real collection, with the branch's own path and depth on the row — NOT a lookups list: one Firestore document caps at 1MB, Riyadh alone has 130-140+ أحياء, and the dashboard rewrites a whole document's items array on every edit.

الناتج: areas and districts live at countries/{code}/places/{name}, cached per country, and the loader stays static-safe

استنتاجsplit out of CO08 so the storage shape can be approved or rejected on its own — the places model is not yet confirmed by him

نجحCO21

The place filter, the breadcrumb and the sell-flow location step are labelled in Saudi and in Qatar

المتوقّع: The LEVEL name comes from the country row, never from a string in code — «المنطقة» means one level in Kuwait and a different one in Saudi, and «المحافظة» does not exist in Qatar at all.

الناتج: 30 buyer/seller files still name a level in code (was 30) — levelName() is the source. RATCHETED, not passing.

استنتاجCO08 covers the DEPTH of the tree and never what the levels are called; both words are hardcoded across dozens of files

نجحCO24

A place slug names both a governorate and a district — الفروانية، الأحمدي، الجهراء، مبارك الكبير are each BOTH in Kuwait today

المتوقّع: The URL resolves to exactly one, and the other is still reachable. It does not: /realestate/in/[area] tries the governorates first and returns on the first hit, so those four DISTRICTS have no page at all — /realestate/in/farwaniya serves the governorate (4 live ads) while the district of the same name (2 live) is unreachable. Found by the places migration, on production data.

الناتج: both levels are looked up and an ambiguous slug resolves to nothing — a 404 is a bug report, silently serving one of two places is not

استنتاجthe location model assumes an Arabic name is a unique key nationwide; it is unique among DISTRICTS and not across LEVELS

الدول9/10
نجحM04

A new country is created, renamed, then activated from the console

المتوقّع: It appears for buyers only once activated; renaming never breaks a link

الناتج: renaming is always allowed and changes no stored value; activation is refused until the country runs a market

قرارك«a page… to create new, activate, or rename.. countries»

نجحCO01

A country is created but left inactive

المتوقّع: Admins can configure it fully; buyers cannot reach it by URL either

الناتج: an inactive country keeps every setting an admin gave it and runs nothing for buyers — both readers return an empty market list for it while the admin page still lists it in full

استنتاجa country is set up before it opens, not while it is open

نجحCO15

A country row is edited after it has been created

المتوقّع: code, dial and refPrefix are frozen ONCE THE COUNTRY HOLDS DATA — an account or an ad — because they are written into stored paths, into identities and onto public ad numbers customers quote back. decimals is never edited on its own, it follows the currency, and an edit to currency or timezone states its consequence out loud.

الناتج: code, dial, refPrefix and currency are all editable while a country is empty and all refused once it holds ads, accounts or money — each with its own reason

استنتاجsplit out of CO02, which is about renaming; mutability is its own question

نجحCO18

The only active country is deactivated

المتوقّع: Refused — deactivation requires at least one other active country, because the site must always have a front door.

الناتج: the last active country cannot be switched off, one of several can, and money still demands a confirm

استنتاجCO03 protects against deleting a country with data; nothing protects against switching the whole product off

نجحCO17

Two admins create the same country code at the same moment, or one creates a code that already exists

المتوقّع: The country code IS the document id, so the second create fails atomically with «already exists» — never two rows, never a silent overwrite of a country someone has already configured.

الناتج: 3 writes, every one addressing countries/{code} — the code IS the id, so a duplicate cannot exist

استنتاجdial code, currency decimals, ad prefix and timezone all hang off that one row

نجحCO03

Someone tries to delete a country that has data

المتوقّع: Refused — deactivation is the answer, deletion is not

الناتج: unused row deletable · used row refused, and the refusal COUNTS what it holds

استنتاجa delete that takes real ads with it cannot be undone

نجحCO02

A country is renamed

المتوقّع: Only the display fields change (nameAr, nameEn, flag); the code is untouched and every stored path, URL and ad number keeps resolving. Which fields are frozen, and from when, is CO15.

الناتج: rename writes the document, purges the countries tag so the site shows it without a deploy, and is audited

استنتاجthe code is written into stored data, URLs and ad numbers

نجحCO04

A country is deactivated while it has live ads and paying dealers

المتوقّع: Listing clocks and subscription periods FREEZE — decided 2026-08-18, matching the rule already set for a business that pauses itself. Switching the country back on restores the exact remaining days. Nothing is refunded and nothing expires while the country is dark, because a dealer must never lose paid visibility to a decision that was not theirs.

الناتج: every country action asks country-lifecycle, re-implements nothing, and returns the refusal SENTENCE

قراركdeactivation stops being a soft action once money is involved

نجحCO14

An admin renames a country in the console, and the site is then served from a build made before that rename

المتوقّع: One answer, not two: either the console edits the shared config and a country change is a DEPLOY, or the registry moves into the database and the country lookup stops being a synchronous throwing function. It cannot be both, and it is written down before any console UI is drawn.

الناتج: countries read from Firestore at runtime · purgeable by tag · seed is the floor when the database is unreachable

استنتاجthe country table is generated at BUILD time into two files and imported synchronously by the gate, the sell flow, the site-country door and the OTP function

لم يُفحصCO25

The admin presses deactivate on a country holding money

المتوقّع: The confirm dialog says the clocks will FREEZE and restore on re-activation, and shows the live counts (N live ads, M paying dealers, X days of subscription about to be frozen) before the button commits.

الناتج: not built yet

قراركsplit out of CO04, which was asking a dialog to state a consequence nobody had decided

المفردات2/2
نجحCO05

A new country is opened

المتوقّع: Its lookups, places and money catalogue are seeded from a template — never empty, and never Kuwait's

الناتج: a country is opened with every money list and every place list of its own, priced in its own currency and placed in its own areas — never empty and never Kuwait’s

استنتاجan empty country renders empty dropdowns; a copied Kuwait renders Kuwaiti governorates

نجحCO19

A second country is opened and the `shared` lookups vertical is read

المتوقّع: `shared` splits: colors, user types and report reasons stay platform-wide; areas and districts become per country. Neither half is inherited by accident, and the gate lists which is which.

الناتج: 34 vocabularies declared · per-country: packages, extendOptions, bumpPacks, rePackages, reExtendOptions, reBumpPacks, corePackages, coreExtendOptions, coreBumpPacks, bizBumpPacks, reBizBumpPacks, bizCoreBumpPacks, reBizCoreBumpPacks, areas, districts · everything else platform-wide

استنتاجtoday `shared` means shared across MARKETS, and it holds the six Kuwaiti governorates next to genuinely global lists

العملة6/6
نجحCO29

A page renders 50 prices

المتوقّع: ONE cached country read for the whole request — the same unstable_cache + tag pattern as lookups, purged when the country is edited. Never a read per amount, and never a read per component

الناتج: every money formatter is pure and takes a currency code; the country is resolved once per request, cached per country and purged on the countries tag — 50 prices cost zero extra reads

قراركhis question: «will this work smoothly? or will slow the loading of the pages?»

نجحCO26

A country is created and its currency is picked from a dropdown

المتوقّع: Every amount in that country reads that one value — no «د.ك» literal survives anywhere, in the site or the functions

الناتج: ZERO hardcoded currency symbols outside comments — every amount on the site, in the dashboard and in the notifications reads its currency from the country row. Still ratcheted, so the next one fails this.

قرارك«when I create a country there is drop down to select the currency, and this is the currency that will be wired in all the places in that world»

نجحCO28

A currency is chosen from the dropdown, or changed later

المتوقّع: Its decimals come WITH it (KWD/BHD/OMR 3, SAR/QAR/AED 2) — decimals live on the CURRENCY, so «SAR with 3 decimals» is not a state a country row can express

الناتج: 20 Arab currencies · 3-decimal true · 2-decimal true · decimals unreachable from a country row

قرارك«the currency drop down should also set up the correct Decimals for each one… if changes, it will reflect the currency and the right Decimals»

نجحCO30

The same amount is printed under a 3-decimal currency and a 2-decimal one

المتوقّع: The ONLY difference between them is the currency row — 12500 prints as 12.500 د.ك and as 1,250.00 ج.م, from one function

الناتج: 12.500 د.ك · 12.500 د.أ · 1,250.00 ج.م

قرارك«if changes, it will reflect the currency and the right Decimals»

نجحCO31

An Arab country outside the Gulf is added later — Jordan, Egypt, Morocco

المتوقّع: Its currency is already in the dropdown with the right decimals: adding the country is a ROW, never a code change

الناتج: all 19 Arab League currencies present with the right decimals (Comoros + Djibouti excluded by ruling)

قرارك«increase the currency table to cover all Arab countries… if in the future I want to add an Arab country, everything is ready»

نجحCO27

The currency is CHANGED on a country that already holds orders or prices

المتوقّع: ALLOWED — decided 2026-08-18 («keep it editable, I'll be careful»), overriding my proposal to lock it at the first order. So the danger has to be made VISIBLE instead of prevented: the confirm dialog states plainly that no stored number is converted, and shows a real amount from that country before and after (15.000 د.ك → 150.00 ر.س), plus how many prices and orders are affected. «Careful» has to be something the screen supports, not something the admin is asked to remember.

الناتج: changeable while empty · refused once ads carry prices in it

قراركhis intent is «edit it and every amount follows», which is right for the LABEL and dangerous for the NUMBER — so the edit window closes before money exists

نجحCO11

الوقت ·A day boundary is computed for a country

المتوقّع: That country's timezone — daily stats, expiry and grace all shift with it

الناتج: the day is the COUNTRY’s: at 20:30Z Kuwait is still the 19th and Dubai is the 20th, and at midday they agree

استنتاجthe business day is not the same everywhere; kuwait-day.ts assumes one

الأرقام2/2
نجحCO07

Existing Kuwaiti ads carry TW- numbers

المتوقّع: A decided answer: renumbered before real ads exist, or TW- kept forever for the old ones

الناتج: 6 countries, each with its own distinct prefix, minted through refId() — no site writes TW- by hand

استنتاجpublic ad numbers cannot be renumbered once customers quote them

نجحCO06

The first ad is posted in a new country

المتوقّع: Its ref comes from that country's OWN counter, printed with that country's prefix (AE-…) — never continuing Kuwait's sequence, and never a number a competitor can read as ad number one.

الناتج: KW-1234 · AE-1234

قرارك«Ad numbers — TW-1234 becomes KW-1234 / AE-1234? yes that's a good idea»

الأسواق2/2
نجحCO10

A country is given a different set of markets from its neighbour

المتوقّع: Accepted as normal — not an exception path

الناتج: 2 different market sets across 6 countries — kw:cars+realestate · qa:none · bh:none · ae:none · sa:none · om:none

قرارك«it is very possible that country have more or less markets than the other»

نجحCO16

A country row exists but no market has been activated in it yet

المتوقّع: Its landing says the country is being prepared, the sell CTA is absent, and nothing falls back to cars.

الناتج: Qatar has no market and cannot be activated — the row is allowed to exist

استنتاجMK10 covers a one-market country and M03 covers one of two; ZERO markets is the state every country is in for its first hour

نجحCO12

الخطأ ·An unknown country code reaches any code path

المتوقّع: It throws or 404s — it never falls back to Kuwait

الناتج: unknown country refused

استنتاجthe same disease as `?? cars`, one level up

نجحCO23

البيانات ·A country is ACTIVATED while its mobile-number ranges are still empty

المتوقّع: The gate FAILS and names it — five of the six countries have no verified regulator range today, and an active country with no range accepts landlines as mobiles.

الناتج: an empty range list refuses («لازم تحدد بدايات أرقام الجوال قبل ما تفت…»), a filled one allows, all 6 countries now carry regulator ranges, and an unknown code is still refused

استنتاجP13 is the standing data gap; this is the door that stops it reaching a customer

لم يُفحصCO13

العطلات ·A country has different weekend or holiday behaviour

المتوقّع: A decision is recorded even if the answer is «no difference» — silence here becomes a bug in expiry

الناتج: not built yet

استنتاجgrace, expiry and «المتبقي» all count days

6

أدراج الدول

18 / 21

Data moves to countries/kw/…; all 113 fetch sites onto one door; the build check lands with it.

الفصل6/6
نجحM05b

A new page fetches listings WITHOUT naming a country

المتوقّع: The BUILD fails — «we were careful» is not the standard

الناتج: 10 listings readers all take the country as their first parameter, and the cache keys on it — omitting it is a type error, not a quiet empty page

قراركthe build check agreed for the drawers

نجحDR03

Every Firestore fetch site is inspected after the step

المتوقّع: No page holds a raw collection handle for country-scoped data: every read of listings, orders, users, accounts, contact events and the money catalogues goes through one country door, and the country-NEUTRAL collections (config, lookups, counters, metrics, audit) are on a printed allow-list with the reason each is neutral. The gate prints the live count on every run — including web/scripts, which writes to production — so the number is evidence, never the criterion.

الناتج: no page or library addresses a root listings/orders/users/accounts/contactEvents collection; every read goes through the country drawer

قرارك«a page never asks for listings, it asks for ITS COUNTRY'S listings»

نجحDR16

A statically rendered page is served to two countries

المتوقّع: Each country gets its own HTML. A static page is ONE cached entry keyed by PATH, so two countries sharing «/cars» share the cache — one of them would be served the other's currency, phone rules and ads. Reading the country from a header instead marks every route dynamic: measured, 55 static routes went to 0. So the country has to be a ROUTE SEGMENT, with the middleware rewriting «/cars» to «/kw/cars» so the visitor's URL never changes. Not a bug while Kuwait is the only open country — the build's country IS the answer then — and a bug the day country two opens.

الناتج: the country is a route segment — each country gets its own cached HTML · 303 prerendered PAGES across 45 static routes + 17 generateStaticParams routes, ratcheted (2026-08-21T08:48)

استنتاجfound by measuring a build after putting requestCountry() in the root layout

نجحDR13

A global /config document is read — the OTP allow-list, the free-ad policy, a market open flag

المتوقّع: It resolves from that country's drawer. Each is ONE global document today, so a second country would inherit Kuwait's SMS allow-list, Kuwait's free credits and live limit, and Kuwait's market flags.

الناتج: 13 config documents declared · 10 follow the country · 3 stay platform-wide

استنتاجfollows M05; the drawers step is where the shared config documents split

نجحM05

Any listings table is opened for Kuwait

المتوقّع: Only Kuwait's drawer is read — another country's rows are not filtered out, they are never fetched

الناتج: 9 group queries over accounts, every one narrowed by country

قرارك«one database, separate drawers»

نجحDR04

Every collectionGroup query is re-examined after the step

المتوقّع: Each is either rewritten to descend from one country, or listed in a PRINTED allow-list with the reason it must cut across countries — and an unlisted one fails the gate. The scan covers web/app, web/lib, web/components, web/scripts AND functions/src: the migration and backfill scripts are the ones that would sweep six countries at once, and the +965-style guard the allow-list copies never leaves web/ today.

الناتج: 55 collectionGroup queries — subscription×6 · accounts×25 · devices×1 · aggregates×2 · users×2 · listings×15 · favorites×2 · orders×2. Each becomes cross-country the day the data moves. RATCHETED.

استنتاجcollectionGroup is the one query shape that ignores the drawer

نجحDR11

الإقامة ·A regulator asks where a country's customer data physically sits

المتوقّع: The honest answer is Dammam, Saudi Arabia — for every country, permanently, because the Firestore location can never be changed. ACCEPTED for all six (owner 2026-08-18). The standing duty that remains: re-check before opening any country whose regulator publishes a data-localisation rule, since that is the one fact that would overturn «one database, separate drawers».

الناتج: recorded in 5 document(s): every country's data sits in me-central2 (Dammam), permanently, and the no-triggers consequence is written beside it

قراركthe Firestore location can NEVER be changed; this is the one thing that overturns one-database

نجحDR06

الفهارس ·The Firestore indexes are reviewed after the move

المتوقّع: Every query still has its index; none was silently left behind on the old path

الناتج: 13 group indexes declared, 31 COLLECTION composites intact

استنتاجa missing index fails the query, not the build

الخلفية3/3
نجحDR08

The 13 scheduled sweeps run after the move

المتوقّع: Each walks every country's drawer, or is explicitly per country — none silently keeps sweeping only Kuwait

الناتج: 13 scheduled jobs; every country-scoped read is a group query or an explicit COUNTRIES loop

استنتاجme-central2 has no triggers, so everything is a sweep and every sweep must be re-read

نجحDR14

A scheduled sweep still reads the old flat path after the migration

المتوقّع: It FAILS LOUDLY. A root collection read returns an EMPTY set once the ads live in the drawer — no exception, no missing-index warning — so every sweep either names the country it swept or asserts a non-empty corpus, and a zero result where the country has live ads is an error, not a number.

الناتج: all 13 sweeps assert a corpus, so an empty walk is an ERROR a person can act on

استنتاجfollows DR08 and DR12; his question was «what if things breaks»

نجحDR19

A scheduled sweep runs and its query no longer matches anything

المتوقّع: It says so. wrapScheduled takes {corpus, acted} and logs at ERROR when the corpus is empty — because «0 acted of 4,312 seen» is a quiet night and «0 of 0» is a job looking at nothing, and the two are identical in the log without that number. The wrapper existed for exactly this failure and was wired to ZERO of the twelve jobs.

الناتج: all 13 scheduled jobs report through wrapScheduled

استنتاجthe audit: «wrapScheduled exists for exactly this failure and is wired to zero jobs»

نجحDR09

النسخ ·A backup is taken

المتوقّع: Per country, restorable per country — the exit door stays real. And the backup gate itself survives the move: the snapshot's required-paths list is a flat set of top-level prefixes matched with startsWith, so on the day paths become countries/kw/… every one of them reports missing and the backup script declares every snapshot broken. It is updated in the SAME commit as the migration, not after.

الناتج: 9 required paths, each per country and per collection; the backup runs

قراركsplitting one country out later must stay mechanical

نجحDR10

الذاكرة ·The listings cache is purged after a script write

المتوقّع: Purged for the right country, and the site stops lying within the same window

الناتج: every cached read goes through perCountryCache, keyed and tagged per country, with the broad tag kept for «purge everything»

قراركthe standing rule: after any script write, purge or the site lies for 10 minutes

نجحDR15

الوقت ·A day-boundary job runs for a country that is not Kuwait

المتوقّع: It runs on THAT country's timezone, which the table already carries. Today there are three different definitions of «the day»: two crons pin Asia/Kuwait, every daily-stats write keys on a Kuwait UTC+3 day key, and the metrics rollup stamps a UTC date.

الناتج: every boundary follows the country table — kw and ae differ by an hour, junk is refused, no Kuwait-day module and no cron pinned to one clock

استنتاجCO11 covers computing a day boundary in the abstract; nothing covers the thirteen sweeps and the write-time day key

الحراسة3/3
نجحDR17

A browser-side module reads or writes a flat root path

المتوقّع: The build FAILS. Zero, not a ratchet — the client conversion was 66 sites done with a regex whose argument pattern excluded quotes, so four files with a literal last segment were skipped and tsc stayed clean, because an untouched path still compiles. A missed client path reads an empty drawer: the user sees an account with no favourites, no history and no wallet.

الناتج: every browser-side Firestore path goes through clientDrawer()

استنتاجfound by grepping after the regex reported success

نجحDR18

A Firestore path is assembled as a STRING rather than through collection()/doc() chaining

المتوقّع: The build FAILS and names it. DR12 and DR17 grep for the collection name as an ARGUMENT, so `adminDb.doc(`users/${uid}/aggregates/inventory`)` was invisible to both — it survived three sweeps and was found by reading the file for an unrelated reason. After the move it reads an empty document forever and the showrooms directory falls back to counting, which is a slower RIGHT answer, so nobody looks.

الناتج: no moved collection is addressed by a string-built path outside scripts/

استنتاجfound by hand during the batch-4 conversion; this is the check that would have found it

نجحDR12

A developer writes a query on the old flat path

المتوقّع: The build fails and NAMES the file and the line — a guard that says «something is wrong» without a location is a guard people disable. It scans web/app, web/lib, web/components, web/scripts and functions/src, because after the move a query on the old flat path returns an EMPTY set with no error and no missing-index warning.

الناتج: 117 root-path accesses (listings 64 · users 37 · orders 16) — every one becomes a silent empty set the day the data moves. RATCHETED, not passing.

استنتاجfollows M05b — the guard is the deliverable, not a wish

نجحDR05

الأمان ·A signed-in Kuwaiti user requests another country's document directly

المتوقّع: Denied by the rules — the drawer is a security boundary, not a convention

الناتج: the country is checked from the auth claim, so a uid cannot own the same document in six drawers

استنتاجa path convention only the app respects is not separation

الترحيل0/2
لم يُفحصDR01

Kuwait's existing data is moved into countries/kw/…

المتوقّع: Nothing is lost, AND it can be put back — two proofs, not one. (a) The pre-move snapshot restores into a scratch project with zero missing, zero differing, zero extra: that is the rollback door. (b) A comparator diffs the pre-move snapshot against a post-move snapshot with the countries/<code>/ prefix stripped — identical document CONTENT and identical per-collection counts recomputed from the stripped paths. The existing round-trip compares full document paths, so it can only prove a same-path restore and is blind to a path-changing move.

الناتج: not built yet

قراركtakeSnapshot + restore --roundtrip already exist and are the only honest proof

لم يُفحصDR02

The migration is interrupted halfway

المتوقّع: It is resumable, and it never leaves half a country readable

الناتج: not built yet

استنتاجa migration that cannot resume will be re-run from zero on live data

لم يُفحصDR07

السرعة ·A Kuwait page is measured before and after the move

المتوقّع: No slower — Firestore is index-backed, so the drawer must cost nothing

الناتج: not built yet

قرارك«speed was never the issue and should not be re-litigated»

7

الدولة في العنوان

17 / 17

kw.teswa.net, the market in the path, and the footer flag becomes the country switch.

العنوان6/6
نجحHO02

Bare teswa.net is opened

المتوقّع: KUWAIT — decided 2026-08-18: «can we keep teswa.net for kuwait and other will be like this ae.teswa.net qa.teswa.net». Kuwait never moves, so no indexed page and no link ever shared in WhatsApp changes. It is a host-only answer, NOT a redirect: teswa.net IS Kuwait.

الناتج: teswa.net is Kuwait by name in both directions; 3 origins exact

قراركthe same disease as the cars default; the root must have an intended answer

نجحHO03

An unknown subdomain (zz.teswa.net) is opened

المتوقّع: The picker or a 404 — never Kuwait

الناتج: 9 hosts: countries resolve, apex/test/dev are named Kuwait, unknown *.teswa.net refused

استنتاجfollows CO12

نجحHO01

ae.teswa.net is opened, and teswa.net is opened

المتوقّع: The country comes from the hostname and nothing else: a subdomain names its country, and the bare domain means Kuwait. No cookie, no header and no guess takes part in that decision.

الناتج: one deployment, one resolver: ae→ae · qa→qa · teswa.net→kw, stamped on every request

قرارك«this direction kw.teswa.net is good»

نجحHO04

An inactive country's subdomain is opened

المتوقّع: A «not open yet» page — not a broken site, and not a leak of its data

الناتج: active is read from the document (absent ≠ active), and a market-less country is a legal state

استنتاجfollows CO01

نجحM06

A visitor switches country from the footer flag or the landing page

المتوقّع: Lands on that country's Teswa; if it does not run the market they were in, its landing page — never a 404

الناتج: the footer flag is the country switch: open countries only, hidden at one, a real navigation to the other host

قرارك«we replace Kuwait flag with a switch/popup to move to other countries»

نجحHO12

The market appears after the country

المتوقّع: kw.teswa.net/cars/… — market is a path segment, country is the host

الناتج: 5 countries run no market yet (qa, bh, ae, sa, om) — the registry answers, and the market switcher hides below two

قرارك«country on the hostname, market as the first path segment»

نجحHO09

المشاركة ·An ad from the UAE is shared

المتوقّع: The link carries ae.teswa.net and opens the UAE site for everyone

الناتج: every share and every canonical builds its origin from the country, via siteOrigin/absoluteUrl

استنتاجfollows HO01

نجحHO10

التطوير ·The site is run on localhost

المتوقّع: A country can be chosen without editing config or faking a hostname

الناتج: localhost, 127.0.0.1 and preview builds resolve to the build country; a country host and an unknown host still differ

استنتاجa dev setup that only serves Kuwait is a dev setup that only tests Kuwait

الجلسة2/2
نجحHO06

A signed-in Kuwaiti user switches to the UAE site

المتوقّع: They are NOT signed in there — separate accounts means separate sessions

الناتج: the session carries a country claim and the rules compare it to the drawer on every write

قرارك«a Kuwaiti dealer opening in Dubai is a different account»

نجحHO07

Cookies and local storage are inspected across two country hosts

المتوقّع: Nothing leaks — no shared session, no shared draft, no shared favourites

الناتج: 5 cookie writes, every one host-only — nothing follows a visitor from one country host to another

استنتاجfollows HO06; a shared cookie is a shared identity

الحراسة2/2
نجحHO13

Someone writes a cookie with a parent domain

المتوقّع: The gate FAILS. It scans both forms — a domain key in a cookie options object, and «domain=» inside a document.cookie string — because two of the four cookie writers build raw strings.

الناتج: 5 cookie writes and not one names a domain — a country host cannot read another's

استنتاجfollows HO06 and HO07: a shared cookie is a shared identity

نجحHO14

Someone writes a country fallback in new code — country ?? 'kw', or any equivalent

المتوقّع: The gate FAILS, exactly as it does for +965. This guard is DOCUMENTED AS ALREADY EXISTING in lib/gcc/site-country.ts and does not exist.

الناتج: one door · no `?? kw` anywhere · no client component resolves its own country

استنتاجthe same disease as the cars default, one level up — and the comment promising the guard is read by every future caller

نجحHO05

التبديل ·A buyer switches country while browsing property in Kuwait

المتوقّع: The same market in the new country if it runs it, its landing if not

الناتج: the switch is in the footer, and the footer is on the door and both market homes

قرارك«the switching between countries can select from the landing page, or from the footer»

نجحHO08

الأرشفة ·Search engines index two countries

المتوقّع: Every absolute URL the site emits resolves against the host that served the request: robots.txt reports its own host and its own sitemap, sitemap.xml carries only that host's URLs and only that country's listings, and every canonical, OpenGraph and JSON-LD URL is its own host. All of it comes from ONE build-time constant today, read in a dozen files including the metadata base and the share-card route — so this is not a three-file edit: either each country is its own deployment with its own site URL, or the constant becomes request-derived and every page that reads it stops being static. That fork is decided with the ISR budget, not after it.

الناتج: each host serves its own sitemap and robots, built from the deployment origin — never a baked-in country

استنتاجduplicate content across hosts is a self-inflicted ranking loss

نجحHO15

الذاكرة ·A browse page is loaded on one country host, then the identical filter query is loaded on another country host inside the cache window

المتوقّع: The second visitor sees their OWN country's ads. Every cached read carries the country in its key — about twenty cached wrappers key only on their argument list today and the filter query has no country in it — and a purge is scoped to one country instead of one global tag.

الناتج: the data cache is keyed AND tagged per country; page-level ISR stays per-path until the /[country] segment ships (DR16)

استنتاجfollows M05 and DR10: one deployment serving six hostnames shares one cache

نجحHO16

التوصيل ·The signup modal validates a phone number in the BROWSER after the country moves to the hostname

المتوقّع: It validates against the HOST's country. The country door is reached from lib/phone.ts, which thirteen client components import, and a client component cannot read the request host — so the country is handed DOWN the tree.

الناتج: one provider, one fallback, and country-current throws rather than guessing; lib/phone.ts still reads the BUILD country until /[country] ships (DR16)

استنتاجthe plan's own claim that step 6 is «a one-file change rather than a 44-file sweep»

نجحHO11

الشهادات ·A new country subdomain is opened for the first time

المتوقّع: TLS and DNS are already in place — a new country is a config step, not an outage

الناتج: 1 open country/countries answer on their own origin: https://teswa.net

استنتاجwildcard certificate or per-host setup must be decided before country #2

8

فرق الإدارة لكل دولة

10 / 12

A team member carries a country list; one person can hold two countries.

الصلاحيات5/6
نجحM07

An admin is granted Kuwait and Bahrain but not Qatar

المتوقّع: Qatar is absent from their switch and unreachable by URL

الناتج: held countries resolve, an unheld one is null in the switch AND redirected by URL

قرارك«I can give admin teams to access more countries if I want»

نجحAD02

A country is removed from an admin while they are using it

المتوقّع: Access ends at the next request — not at the next login

الناتج: countries are read from the document on every request, and a failed read locks out rather than opens up

استنتاجa revocation that waits for a logout is not a revocation

نجحAD05

Roles are assigned per country

المتوقّع: A person can be an editor in Kuwait and a viewer in Bahrain

الناتج: one role per country, held in the document; the claim carries membership only and nothing falls open to owner

استنتاجfollows the permission-system framing rather than a flat country list

نجحAD08

The sole owner account is considered

المتوقّع: The sole owner holds every country, and the UI says so in words rather than leaving it to be inferred from a full list. The owner fallback for an unrecognised role is deliberate back-compat for the sole admin — after countries it must not become an accidental ALL-COUNTRY owner, so the country set is resolved from the claim independently of that fallback.

الناتج: the sole owner is owner in all 6 countries, present and future, and the UI says so in words

استنتاجthe existing sole-admin concept

نجحAD01

That admin calls the API directly for Qatar

المتوقّع: Refused server-side — the UI is not the permission

الناتج: 6 of 13 guarded routes are country-aware; none reads a drawer without checking the country

استنتاجhiding a tab is not a permission system

لم يُفحصAD10

The S7 round is run against production, where the dashboard runs in open mode

المتوقّع: The gate REFUSES to report any permission row as anything but not-checked: with the open flag on, every role resolves to owner and every country permission resolves to «sees everything». The round runs on a preview deploy with the flag off, and the gate reads the flag and records which it was.

الناتج: REFUSED — TESWA_DASHBOARD_OPEN is on, so every role resolves to owner and every country to «sees everything». No permission row can be checked here. Run the S7 round on a deploy with the flag OFF; the resolver itself is asserted by `pnpm admin:countries:proof`, which bypasses the flag entirely.

استنتاجthe dashboard-guard check exists because two guard mismatches were «invisible in review and invisible in production»

التبديل1/2
نجحAD03

An admin with two countries uses the switch at the top of the dashboard

المتوقّع: Every page reloads into the new country; nothing keeps showing the old one

الناتج: a real cross-origin navigation that keeps the path — no client state crosses

قرارك«a country switch sits at the top… treat them like they are separated fully»

لم يُفحصAD04

An admin switches country while a dialog or an unsaved edit is open

المتوقّع: They are warned — a half-finished edit must not be applied to another country

الناتج: not built yet

استنتاجthe close-guard pattern already exists in the sell flows

الفريق2/2
نجحAD07

A team member is invited

المتوقّع: Invited INTO named countries; there is no «all countries» by default

الناتج: an invite names its countries; holding none is refused at creation, never inferred as «all»

استنتاجa default of all is how a Kuwait hire ends up editing Saudi

نجحAD11

The last admin holding a country is removed, or has that country taken away

المتوقّع: Refused, or the owner is warned by name — nobody can reach that country any more and only the owner can re-grant it.

الناتج: all three ops that reduce a holding ask orphanedBy first, and the refusal names the countries

استنتاجAD02 covers revocation taking effect; nothing covers revocation leaving a country unattended

نجحAD09

الهوية ·A team member is added whose phone belongs to another country

المتوقّع: Allowed, and written to the audit log — decided 2026-08-18. Staff are NOT customers: one admin may hold Kuwait and Bahrain and cannot carry two phones. The customer rule («a Kuwaiti number posts only in Kuwait») stays untouched; this is an explicit, recorded exception for the team, not a loosening of it.

الناتج: a foreign staff number is allowed, stamped and audited, while the customer rule stays strict

قراركthe customer rule («a Kuwaiti number posts only in Kuwait») cannot be the staff rule if one person holds two countries

نجحAD06

السجلّ ·An admin action is recorded

المتوقّع: The country is on the record — «who did what» is incomplete without «where»

الناتج: 5 audit writers, every one stamping its country, and the audit page shows it

استنتاجthe audit log already records the actor

9

لوحة الدول

11 / 11

One page reading six scorecards — the only surface allowed to see across countries.

نجحCN05

المال ·Money is shown across countries

المتوقّع: Each in its own currency, never summed

الناتج: both order writers take their country from the drawer they write into, never from the build

استنتاجfollows MO11 and MO12

اللوحة9/9
نجحCN09

One aggregate in a country's rollup fails while the rest succeed

المتوقّع: That figure reads «—», never 0. The rollup swallows a failed count into a zero today, so a partial failure writes a scorecard that is structurally valid and numerically a lie.

الناتج: a failed aggregate writes null and the console renders «—» — never a plausible 0

استنتاجCN02 covers a scorecard that is STALE; this is one that is FRESH AND WRONG

نجحCN03

A new country is created

المتوقّع: It appears on the console automatically, with empty figures rather than absence

الناتج: 6 countries, identical on both sides — a new country is one row in shared/config

استنتاجan absent country reads as a broken page

نجحCN04

An inactive country is viewed

المتوقّع: Still listed, marked inactive — it is being prepared, and preparation needs visibility

الناتج: an inactive country still renders, and says «غير مفعّلة / لم يُقس بعد» instead of showing zeros

استنتاجfollows CO01

نجحCN07

The console is loaded with six countries

المتوقّع: Fast — six scorecard reads, not six country sweeps

الناتج: 6 countries, all from the table — adding one adds a row, not a code change

استنتاجfollows CN01

نجحCN10

The console asks for a country's scorecard on its first day, before any rollup has ever run

المتوقّع: «لم يُقس بعد», not zeros — and the gate names any country whose latest scorecard is older than 48 hours. The WRITE side has an owner: there are no triggers here, so a per-country scorecard exists only if a scheduled sweep writes it.

الناتج: a country with no scorecard yet says so, rather than rendering a row of zeros

استنتاجCN01 to CN07 are seven rows about the reader and none about the writer

نجحCN02

A scorecard is stale because a sweep failed

المتوقّع: The staleness is SHOWN — an old number presented as current is worse than a gap

الناتج: every card renders its own age, so a stale number cannot pass for a current one

استنتاجthe same habit as this gate: name what is not fresh

نجحCN08

A country that carried live ads and paying dealers is deactivated

المتوقّع: Its scorecard FREEZES at the last real figures and is labelled as of that date — it does not drop to zero.

الناتج: an inactive country’s card freezes, and a country carrying ads or dealers cannot be deleted

استنتاجfollows CO04 and CN02; CN04 covers a country that is inactive because it is being prepared, not one that was switched off

نجحM08

The cross-country console is opened

المتوقّع: Six scorecards, no raw listing reads, and it says how old the numbers are

الناتج: one page, pre-aggregated scorecards only, behind a role gate

قرارك«a page to show data/performance for the countries»

نجحCN01

The console is asked where its numbers come from

المتوقّع: Pre-aggregated daily scorecards — it never queries a country's listings

الناتج: every figure traces to one scheduled sweep, and every card shows how old it is

قرارك«the cross-country console is the ONLY reader across countries and it reads daily scorecards»

نجحCN06

الصلاحيات ·An admin who holds only Kuwait opens the console

المتوقّع: A DECISION: is the console owner-only, or does it show only their countries?

الناتج: the door checks your role, the contents check what you HOLD — a Kuwait-only admin sees Kuwait

استنتاجit is the one surface that crosses the fence his architecture draws

جولتان لكل سيناريو: واحدة فور انتهاء خطوته، وواحدة كاملة بعد انتهاء كل الخطوات — لأن خطوة متأخرة قد تكسر شيئًا أثبتته خطوة سابقة، ولا يكشف ذلك إلا إعادة التشغيل الكاملة. الخطوة رقم 0 «ما ينكسر» ليست خطوة تنتهي — تُعاد في كل جولة، لأن الموقع الشغّال اليوم هو أكثر شيء معرّض للكسر.
pnpm gcc:gate جولة كاملة · pnpm gcc:gate S1-phones جولة خطوة.

🔧🧪