لوحة الخليج — الخطة والفحص
كل سيناريو هنا مأخوذ من قرار قلته، أو من استنتاج مكتوب عليه «استنتاج» عشان تقدر ترفضه. والنتيجة أسفله ناتج تشغيل حقيقي — لا علامة كتبتها بيدي. وما لم يُفحص بعد مكتوب بوضوح، لأن جدارًا أخضر يخفي بندًا واحدًا غير مفحوص أسوأ من جدار أحمر.
هذه النتيجة قديمة — ما تنطبق على الكود المنشور
التشغيل كان على كود ثاني (07458af5) والمنشور الآن (2bb07510). كل بند تحت صار «لم يُفحص» لين يعاد التشغيل — لأن أخضر على كود ثاني أسوأ من أحمر.
المجموع
247
نجح
0
فشل
0
بالعين
0
لم يُفحص
247
آخر تشغيل: 21 أغسطس 2026، 12:57 م · النطاق: جولة كاملة · 174 بند مبني على استنتاج — راجعها وارفض اللي ما يعجبك
ما ينكسر — الموقع الحالي
0 / 23The 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.
الخلفية ·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
السجلّ ·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
المال ·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
الطلب ·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
الفراغ ·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
اللوحة ·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
الروابط ·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
الإعلان ·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
النشر ·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 be486159 — same as HEAD
قراركthe standing rule: production may be rolled back; check the deployment, not the branch
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
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
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
المختبر ·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
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
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
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
السرعة ·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
البيع ·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
الحساب ·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
الأمان ·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
اللغة ·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
البحث ·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
التراجع ·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
أرقام الخليج
0 / 38A country → dial code + length table. Before this, an 8-digit Bahraini number was silently filed as Kuwaiti.
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»
A Kuwaiti number (8 digits) is entered on Kuwait Teswa
المتوقّع: Accepted and normalised to +965XXXXXXXX
الناتج: +96550000000
قرارك«+965 only for Kuwait Teswa»
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»
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»
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
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»
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»
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
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»
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
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»
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
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»
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
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
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
التخزين ·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
الحراسة ·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
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
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
The number is pasted in 00-form: 0096550000000
المتوقّع: Accepted as +96550000000
الناتج: +96550000000
استنتاجthe 00 form is what Gulf phones store for saved international contacts
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
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
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
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
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/
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
الاختبار ·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
الدولة ·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
الهوية ·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
التصادم ·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
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
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
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
الواتساب ·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
السجلّ ·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
الأرقام الإضافية ·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
البيانات ·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
العملة لكل دولة
0 / 35Every 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).
التحويل ·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
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
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»
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»
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
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
البحث ·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
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
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
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
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
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
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
الحساب ·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
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
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
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
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
التصفّح ·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
الاشتراك ·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
الفاتورة ·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
التصدير ·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
الفراغ ·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
الحراسة ·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
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»
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»
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
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.»
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
المشاركة ·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
الاسترجاع ·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
التسعير الذكي ·«السعر المقترح» 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»
المجاني ·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
البيع ·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
الوحدة ·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
الأسواق قائمة لا كود
0 / 21A country can run a different set of markets. Today the two markets are a hardcoded pair across 260 places.
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»
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
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`
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
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
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»
المال ·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
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»
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
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
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
الأمان ·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
المفردات ·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
الإحصاء ·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
الهوية ·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
اللغة ·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
الترتيب ·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
الإدارة ·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
الروابط ·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
الملف ·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
المفضلة ·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
السيارات سوق مثل غيره
0 / 32Cars 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.
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»)
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
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
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
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
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
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
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
الخطأ ·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
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
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
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
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
الحفظ ·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
الجذر ·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»
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
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
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
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
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
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
التنقّل ·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»
السجلّ ·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
التطبيق ·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
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
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
البيع ·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»
الهوية ·The header logo is clicked from anywhere on the site
المتوقّع: The country landing, not the cars browse
الناتج: not built yet
استنتاجfollows CA01
المشاركة ·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
الإدارة ·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
السرعة ·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
الفراغ ·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
الدول قائمة
0 / 37The registry row + the page to create, rename, activate and deactivate a country.
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»
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»
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»
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
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
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 الحي»
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
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
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
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
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
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»
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
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
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
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
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
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
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
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
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
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
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
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?»
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»
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»
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»
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»
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
الوقت ·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
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
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»
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»
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
الخطأ ·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
البيانات ·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
العطلات ·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
أدراج الدول
0 / 21Data moves to countries/kw/…; all 113 fetch sites onto one door; the build check lands with it.
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
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»
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-21T09:39)
استنتاجfound by measuring a build after putting requestCountry() in the root layout
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
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»
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
الإقامة ·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
The migration is interrupted halfway
المتوقّع: It is resumable, and it never leaves half a country readable
الناتج: every ordered field on a drawered collection is indexed at BOTH scopes (5 fields) — a dashboard reading one country and a sweep reading all of them both work
استنتاجa migration that cannot resume will be re-run from zero on live data
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
الفهارس ·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
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
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»
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»
النسخ ·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
الذاكرة ·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
الوقت ·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
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
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
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
الأمان ·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
السرعة ·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»
الدولة في العنوان
0 / 17kw.teswa.net, the market in the path, and the footer flag becomes the country switch.
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
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
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»
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
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»
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»
المشاركة ·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
التطوير ·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
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»
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
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
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
التبديل ·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»
الأرشفة ·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
الذاكرة ·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
التوصيل ·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»
الشهادات ·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
فرق الإدارة لكل دولة
0 / 12A team member carries a country list; one person can hold two countries.
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
الناتج: 15 categories over 4 sections (performance → ops → settings → system), 47 pages, 10 old addresses still landing, and a distinct colour for every category however many markets open
استنتاجthe close-guard pattern already exists in the sell flows
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»
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»
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
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
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
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
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»
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
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
الهوية ·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
السجلّ ·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
لوحة الدول
0 / 11One page reading six scorecards — the only surface allowed to see across countries.
المال ·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
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
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
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
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
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
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
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
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»
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»
الصلاحيات ·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 جولة خطوة.