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

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

المجموع

241

نجح

88

فشل

0

بالعين

4

لم يُفحص

149

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

0

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

7 / 23

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

نجحRG07

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

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

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

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

نجحRG08

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

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

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

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

نجحRG16

النشر ·A step is deployed

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

الناتج: serving 62d55f0b — same as HEAD

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

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

The whole site is typechecked after a step lands

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

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

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

نجحRG02

The site is linted after a step lands

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

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

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

نجحRG03

Cloud Functions are typechecked after a step touches them

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

الناتج: functions tsc clean

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

نجحLAB01

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

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

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

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

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

The 48-surface audit is run against production

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

الناتج: not built yet

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

لم يُفحصRG05

The browser suite is run against production

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

الناتج: not built yet

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

لم يُفحصRG23

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

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

الناتج: not built yet

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

لم يُفحصRG06

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

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

الناتج: not built yet

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

لم يُفحصRG09

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

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

الناتج: not built yet

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

لم يُفحصRG10

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

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

الناتج: not built yet

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

لم يُفحصRG11

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

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

الناتج: not built yet

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

لم يُفحصRG12

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

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

الناتج: not built yet

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

لم يُفحصRG13

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

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

الناتج: not built yet

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

لم يُفحصRG15

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

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

الناتج: not built yet

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

لم يُفحصRG17

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

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

الناتج: not built yet

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

لم يُفحصRG20

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

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

الناتج: not built yet

استنتاج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

لم يُفحصRG19

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

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

الناتج: not built yet

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

لم يُفحصRG18

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

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

الناتج: not built yet

استنتاج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

لم يُفحصRG21

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

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

الناتج: not built yet

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

لم يُفحصRG22

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

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

الناتج: not built yet

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

1

أرقام الخليج

33 / 38

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

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

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

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

الناتج: refused

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

نجحP02

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

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

الناتج: +96550000000

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

نجحP03

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

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

الناتج: refused

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

نجحP04

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

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

الناتج: +971501234567

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

نجحP05

The same phone number registers twice on the SAME country

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

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

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

نجحP06

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

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

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

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

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

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

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

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

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

نجحP34

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

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

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

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

نجحP30

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

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

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

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

لم يُفحصP38

One country's SMS volume spikes, or is pumped

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

الناتج: not built yet

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

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

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

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

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

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

بالعينP09

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

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

الناتج: needs a human eye

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

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

لم يُفحصP37

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

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

الناتج: not built yet

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

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

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

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

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

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

نجحP11

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

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

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

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

نجحP12

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

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

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

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

نجحP14

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

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

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

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

نجحP15

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

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

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

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

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

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

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

الناتج: +96550000000

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

نجحP17

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

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

الناتج: all 4 forms accepted

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

نجحP18

The number is pasted in 00-form: 0096550000000

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

الناتج: +96550000000

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

نجحP19

More digits are pasted than the country has

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

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

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

نجحP20

The field is empty, or holds only spaces

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

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

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

نجحP21

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

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

الناتج: kuwait landline refused · kuwait mobile accepted · qatar rule none yet (length only)

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

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

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

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

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

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

نجحP35

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

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

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

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

نجحP27

The wallet desk searches accounts by a phone PREFIX

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

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

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

نجحP28

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

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

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

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

نجحP32

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

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

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

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

نجحP33

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

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

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

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

نجحP22

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

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

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

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

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

An account created before this change is loaded

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

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

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

نجحP24

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

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

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

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

لم يُفحصP36

Every phone already stored in the live database is swept

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

الناتج: not built yet

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

نجحP25

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

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

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

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

نجحP29

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

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

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

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

نجحP31

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

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

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

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

لم يُفحصP13

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

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

الناتج: no mobile rule yet: Qatar, Bahrain, UAE, Saudi Arabia, Oman — length + dial code only

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

2

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

14 / 35

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

نجحMO02

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

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

الناتج: baseline 42 files still carry a currency literal. NOT passing, RATCHETED: no NEW file may acquire one.

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

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

The payment gateway is called

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

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

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

نجحMO31

A dealer in Saudi pays for a subscription

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

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

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

نجحMO24

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

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

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

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

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

An order is created and paid

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

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

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

نجحMO06

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

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

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

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

نجحMO16

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

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

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

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

نجحMO19

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

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

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

استنتاجfollows MO05

التخزين1/3
نجحMO26

An AD's price is stored

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

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

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

لم يُفحصMO03

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

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

الناتج: not built yet

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

لم يُفحصMO27

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

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

الناتج: not built yet

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

نجحMO29

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

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

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

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

نجحMO32

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

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

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

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

نجحMO13

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

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

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

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

نجحMO34

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

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

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

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

العرض1/3
نجحM02

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

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

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

قرارك«Same for currency»

لم يُفحصMO01

An ad price is printed anywhere on the site

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

الناتج: not built yet

قرارك«Same for currency»

لم يُفحصMO28

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

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

الناتج: not built yet

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

لم يُفحصMO04

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

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

الناتج: not built yet

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

الباقات0/2
لم يُفحصMO07

Subscription tiers are read in Qatar

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

الناتج: not built yet

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

لم يُفحصMO08

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

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

الناتج: not built yet

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

المحفظة0/2
لم يُفحصMO09

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

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

الناتج: not built yet

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

لم يُفحصMO10

Free-ad credits (adCredits) are shown beside money

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

الناتج: not built yet

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

الإدارة0/3
لم يُفحصMO11

The dashboard shows revenue for a country

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

الناتج: not built yet

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

لم يُفحصMO12

The cross-country console shows money

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

الناتج: not built yet

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

لم يُفحصMO25

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

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

الناتج: not built yet

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

لم يُفحصMO14

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

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

الناتج: not built yet

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

لم يُفحصMO15

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

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

الناتج: not built yet

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

لم يُفحصMO17

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

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

الناتج: not built yet

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

لم يُفحصMO20

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

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

الناتج: not built yet

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

الأرقام0/2
بالعينMO21

Any amount is rendered in Arabic RTL

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

الناتج: needs a human eye

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

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

لم يُفحصMO33

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

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

الناتج: not built yet

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

لم يُفحصMO22

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

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

الناتج: not built yet

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

لم يُفحصMO23

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

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

الناتج: not built yet

استنتاج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

لم يُفحصMO30

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

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

الناتج: not built yet

استنتاج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

3

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

8 / 20

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

نجحMK19

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

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

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

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

البحث1/3
نجحMK18

A search is filtered by country

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

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

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

لم يُفحصMK09

Search is performed inside a market

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

الناتج: not built yet

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

لم يُفحصMK17

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

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

الناتج: not built yet

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

الأسواق2/3
نجحMK01

A THIRD market is added to a country

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

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

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

نجحM03

A country is configured with property only

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

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

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

لم يُفحصMK15

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

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

الناتج: not built yet

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

نجحMK04

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

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

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

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

نجحMK07

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

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

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

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

نجحMK10

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

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

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

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

نجحMK03

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

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

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

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

لم يُفحصMK02

الحراسة ·Someone writes `world === 'cars' ? … : …` in new code

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

الناتج: not built yet

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

لم يُفحصMK05

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

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

الناتج: not built yet

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

لم يُفحصMK06

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

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

الناتج: not built yet

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

الإطلاق0/2
لم يُفحصMK08

A market is created but not yet activated in a country

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

الناتج: not built yet

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

لم يُفحصMK16

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

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

الناتج: not built yet

استنتاج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»

لم يُفحصMK11

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

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

الناتج: not built yet

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

لم يُفحصMK12

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

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

الناتج: not built yet

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

لم يُفحصMK13

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

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

الناتج: not built yet

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

لم يُفحصMK14

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

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

الناتج: not built yet

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

4

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

3 / 32

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

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

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

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

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

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

لم يُفحصCA25

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

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

الناتج: not built yet

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

لم يُفحصCA22

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

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

الناتج: not built yet

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

الروابط1/3
نجحMK20

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

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

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

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

لم يُفحصCA02

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

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

الناتج: not built yet

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

لم يُفحصCA03

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

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

الناتج: not built yet

استنتاجfollows CA02

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

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

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

الناتج: 129 cars defaults — fills-a-blank 36 · converts-a-value 54 · hardcoded-default 39. RATCHETED, not passing.

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

لم يُفحصCA30

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

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

الناتج: not built yet

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

لم يُفحصCA01

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

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

الناتج: not built yet

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

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

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

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

الناتج: not built yet

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

لم يُفحصCA26

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

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

الناتج: not built yet

استنتاج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

الأمان0/2
لم يُفحصCA06

firestore.rules evaluate a write with no world on it

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

الناتج: not built yet

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

لم يُفحصCA28

A business account whose market was never recorded posts an ad

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

الناتج: not built yet

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

النشر0/2
لم يُفحصCA07

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

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

الناتج: not built yet

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

لم يُفحصCA29

The route move is deployed to production

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

الناتج: not built yet

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

لم يُفحصCA08

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

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

الناتج: not built yet

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

الإعلان0/2
لم يُفحصCA09

An old listing document has no world field

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

الناتج: not built yet

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

لم يُفحصCA27

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

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

الناتج: not built yet

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

لم يُفحصCA10

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

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

الناتج: not built yet

استنتاجfollows CA01

بالعينCA11

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

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

الناتج: needs a human eye

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

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

الأرشفة0/2
بالعينCA12

The sitemap and robots are generated

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

الناتج: needs a human eye

استنتاجfollows CA01 and CA02

لم يُفحصCA23

robots.ts is read after the routes move

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

الناتج: not built yet

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

لم يُفحصCA13

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

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

الناتج: not built yet

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

لم يُفحصCA14

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

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

الناتج: not built yet

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

لم يُفحصCA15

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

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

الناتج: not built yet

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

البحث0/2
لم يُفحصCA16

Search runs after the demotion

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

الناتج: not built yet

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

لم يُفحصCA24

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

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

الناتج: not built yet

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

لم يُفحصCA17

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

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

الناتج: not built yet

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

لم يُفحصCA18

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

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

الناتج: not built yet

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

لم يُفحصCA19

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

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

الناتج: not built yet

استنتاجfollows CA01

لم يُفحصCA20

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

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

الناتج: not built yet

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

لم يُفحصCA31

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

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

الناتج: not built yet

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

5

الدول قائمة

15 / 32

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

نجحCO12

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

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

الناتج: unknown country refused

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

العملة4/6
نجحCO28

A currency is chosen from the dropdown, or changed later

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

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

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

نجحCO30

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

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

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

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

نجحCO31

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

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

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

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

نجحCO27

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

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

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

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

لم يُفحصCO26

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

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

الناتج: not built yet

قرارك«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»

لم يُفحصCO29

A page renders 50 prices

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

الناتج: not built yet

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

نجحCO23

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

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

الناتج: Kuwait may open · Qatar refused («لازم تحدد بدايات أرقام الجوال قبل ما تفتح الدو…») · an unknown code refused

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

الدول4/10
نجحCO03

Someone tries to delete a country that has data

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

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

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

نجحCO02

A country is renamed

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

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

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

نجحCO04

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

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

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

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

نجحCO14

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

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

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

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

لم يُفحصM04

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

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

الناتج: not built yet

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

لم يُفحصCO01

A country is created but left inactive

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

الناتج: not built yet

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

لم يُفحصCO25

The admin presses deactivate on a country holding money

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

الناتج: not built yet

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

لم يُفحصCO15

A country row is edited after it has been created

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

الناتج: not built yet

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

لم يُفحصCO17

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

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

الناتج: not built yet

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

لم يُفحصCO18

The only active country is deactivated

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

الناتج: not built yet

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

الأسواق1/2
نجحCO16

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

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

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

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

لم يُفحصCO10

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

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

الناتج: not built yet

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

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

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

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

الناتج: 34 vocabularies declared · per-country: areas, districts · everything else platform-wide

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

لم يُفحصCO05

A new country is opened

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

الناتج: not built yet

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

الأماكن2/6
نجحCO21

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

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

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

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

نجحCO24

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

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

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

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

لم يُفحصCO08

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

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

الناتج: not built yet

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

لم يُفحصCO09

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

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

الناتج: not built yet

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

لم يُفحصCO20

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

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

الناتج: not built yet

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

لم يُفحصCO22

A country's places are stored

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

الناتج: not built yet

استنتاج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

الأرقام1/2
نجحCO06

The first ad is posted in a new country

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

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

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

لم يُفحصCO07

Existing Kuwaiti ads carry TW- numbers

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

الناتج: not built yet

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

لم يُفحصCO11

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

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

الناتج: not built yet

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

لم يُفحصCO13

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

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

الناتج: not built yet

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

6

أدراج الدول

7 / 21

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

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

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

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

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

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

نجحDR18

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

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

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

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

نجحDR12

A developer writes a query on the old flat path

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

الناتج: 122 root-path accesses (listings 66 · users 40 · 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

الخلفية1/3
نجحDR19

A scheduled sweep runs and its query no longer matches anything

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

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

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

لم يُفحصDR08

The 13 scheduled sweeps run after the move

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

الناتج: not built yet

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

لم يُفحصDR14

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

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

الناتج: not built yet

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

الفصل2/6
لم يُفحصDR16

A statically rendered page is served to two countries

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

الناتج: 6 countries in the table, ONE active. The build's country is the right answer for a static page while that stays true. Before country two goes active, either /[country] carries it or the 55 static routes have to go dynamic.

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

نجحDR13

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

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

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

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

نجحDR04

Every collectionGroup query is re-examined after the step

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

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

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

لم يُفحصM05

Any listings table is opened for Kuwait

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

الناتج: not built yet

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

لم يُفحصM05b

A new page fetches listings WITHOUT naming a country

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

الناتج: not built yet

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

لم يُفحصDR03

Every Firestore fetch site is inspected after the step

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

الناتج: not built yet

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

نجحDR05

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

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

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

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

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

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

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

الناتج: not built yet

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

لم يُفحصDR02

The migration is interrupted halfway

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

الناتج: not built yet

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

لم يُفحصDR06

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

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

الناتج: not built yet

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

لم يُفحصDR07

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

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

الناتج: not built yet

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

لم يُفحصDR09

النسخ ·A backup is taken

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

الناتج: not built yet

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

لم يُفحصDR10

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

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

الناتج: not built yet

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

لم يُفحصDR11

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

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

الناتج: not built yet

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

لم يُفحصDR15

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

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

الناتج: not built yet

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

7

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

1 / 17

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

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

Someone writes a country fallback in new code — country ?? 'kw', or any equivalent

المتوقّع: The gate FAILS, exactly as it does for +965. This guard is DOCUMENTED AS ALREADY EXISTING in lib/gcc/site-country.ts and does not exist.

الناتج: one door · no `?? kw` anywhere · no client component resolves its own country

استنتاجthe same disease as the cars default, one level up — and the comment promising the guard is read by every future caller

لم يُفحصHO13

Someone writes a cookie with a parent domain

المتوقّع: The gate FAILS. It scans both forms — a domain key in a cookie options object, and «domain=» inside a document.cookie string — because two of the four cookie writers build raw strings.

الناتج: not built yet

استنتاجfollows HO06 and HO07: a shared cookie is a shared identity

العنوان0/6
لم يُفحصM06

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

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

الناتج: not built yet

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

لم يُفحصHO01

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

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

الناتج: not built yet

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

لم يُفحصHO02

Bare teswa.net is opened

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

الناتج: not built yet

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

لم يُفحصHO03

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

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

الناتج: not built yet

استنتاجfollows CO12

لم يُفحصHO04

An inactive country's subdomain is opened

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

الناتج: not built yet

استنتاجfollows CO01

لم يُفحصHO12

The market appears after the country

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

الناتج: not built yet

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

لم يُفحصHO05

التبديل ·A buyer switches country while browsing property in Kuwait

المتوقّع: The same market in the new country if it runs it, its landing if not

الناتج: not built yet

قرارك«the switching between countries can select from the landing page, or from the footer»

الجلسة0/2
لم يُفحصHO06

A signed-in Kuwaiti user switches to the UAE site

المتوقّع: They are NOT signed in there — separate accounts means separate sessions

الناتج: not built yet

قرارك«a Kuwaiti dealer opening in Dubai is a different account»

لم يُفحصHO07

Cookies and local storage are inspected across two country hosts

المتوقّع: Nothing leaks — no shared session, no shared draft, no shared favourites

الناتج: not built yet

استنتاجfollows HO06; a shared cookie is a shared identity

لم يُفحصHO08

الأرشفة ·Search engines index two countries

المتوقّع: Every absolute URL the site emits resolves against the host that served the request: robots.txt reports its own host and its own sitemap, sitemap.xml carries only that host's URLs and only that country's listings, and every canonical, OpenGraph and JSON-LD URL is its own host. All of it comes from ONE build-time constant today, read in a dozen files including the metadata base and the share-card route — so this is not a three-file edit: either each country is its own deployment with its own site URL, or the constant becomes request-derived and every page that reads it stops being static. That fork is decided with the ISR budget, not after it.

الناتج: not built yet

استنتاجduplicate content across hosts is a self-inflicted ranking loss

لم يُفحصHO09

المشاركة ·An ad from the UAE is shared

المتوقّع: The link carries ae.teswa.net and opens the UAE site for everyone

الناتج: not built yet

استنتاجfollows HO01

لم يُفحصHO10

التطوير ·The site is run on localhost

المتوقّع: A country can be chosen without editing config or faking a hostname

الناتج: not built yet

استنتاجa dev setup that only serves Kuwait is a dev setup that only tests Kuwait

لم يُفحصHO11

الشهادات ·A new country subdomain is opened for the first time

المتوقّع: TLS and DNS are already in place — a new country is a config step, not an outage

الناتج: not built yet

استنتاجwildcard certificate or per-host setup must be decided before country #2

لم يُفحصHO15

الذاكرة ·A browse page is loaded on one country host, then the identical filter query is loaded on another country host inside the cache window

المتوقّع: The second visitor sees their OWN country's ads. Every cached read carries the country in its key — about twenty cached wrappers key only on their argument list today and the filter query has no country in it — and a purge is scoped to one country instead of one global tag.

الناتج: not built yet

استنتاجfollows M05 and DR10: one deployment serving six hostnames shares one cache

لم يُفحصHO16

التوصيل ·The signup modal validates a phone number in the BROWSER after the country moves to the hostname

المتوقّع: It validates against the HOST's country. The country door is reached from lib/phone.ts, which thirteen client components import, and a client component cannot read the request host — so the country is handed DOWN the tree.

الناتج: not built yet

استنتاجthe plan's own claim that step 6 is «a one-file change rather than a 44-file sweep»

8

فرق الإدارة لكل دولة

0 / 12

A team member carries a country list; one person can hold two countries.

الصلاحيات0/6
لم يُفحصM07

An admin is granted Kuwait and Bahrain but not Qatar

المتوقّع: Qatar is absent from their switch and unreachable by URL

الناتج: not built yet

قرارك«I can give admin teams to access more countries if I want»

لم يُفحصAD01

That admin calls the API directly for Qatar

المتوقّع: Refused server-side — the UI is not the permission

الناتج: not built yet

استنتاجhiding a tab is not a permission system

لم يُفحصAD02

A country is removed from an admin while they are using it

المتوقّع: Access ends at the next request — not at the next login

الناتج: not built yet

استنتاجa revocation that waits for a logout is not a revocation

لم يُفحصAD05

Roles are assigned per country

المتوقّع: A person can be an editor in Kuwait and a viewer in Bahrain

الناتج: not built yet

استنتاجfollows the permission-system framing rather than a flat country list

لم يُفحصAD08

The sole owner account is considered

المتوقّع: The sole owner holds every country, and the UI says so in words rather than leaving it to be inferred from a full list. The owner fallback for an unrecognised role is deliberate back-compat for the sole admin — after countries it must not become an accidental ALL-COUNTRY owner, so the country set is resolved from the claim independently of that fallback.

الناتج: not built yet

استنتاجthe existing sole-admin concept

لم يُفحصAD10

The S7 round is run against production, where the dashboard runs in open mode

المتوقّع: The gate REFUSES to report any permission row as anything but not-checked: with the open flag on, every role resolves to owner and every country permission resolves to «sees everything». The round runs on a preview deploy with the flag off, and the gate reads the flag and records which it was.

الناتج: not built yet

استنتاجthe dashboard-guard check exists because two guard mismatches were «invisible in review and invisible in production»

التبديل0/2
لم يُفحصAD03

An admin with two countries uses the switch at the top of the dashboard

المتوقّع: Every page reloads into the new country; nothing keeps showing the old one

الناتج: not built yet

قرارك«a country switch sits at the top… treat them like they are separated fully»

لم يُفحصAD04

An admin switches country while a dialog or an unsaved edit is open

المتوقّع: They are warned — a half-finished edit must not be applied to another country

الناتج: not built yet

استنتاجthe close-guard pattern already exists in the sell flows

لم يُفحصAD06

السجلّ ·An admin action is recorded

المتوقّع: The country is on the record — «who did what» is incomplete without «where»

الناتج: not built yet

استنتاجthe audit log already records the actor

الفريق0/2
لم يُفحصAD07

A team member is invited

المتوقّع: Invited INTO named countries; there is no «all countries» by default

الناتج: not built yet

استنتاجa default of all is how a Kuwait hire ends up editing Saudi

لم يُفحصAD11

The last admin holding a country is removed, or has that country taken away

المتوقّع: Refused, or the owner is warned by name — nobody can reach that country any more and only the owner can re-grant it.

الناتج: not built yet

استنتاجAD02 covers revocation taking effect; nothing covers revocation leaving a country unattended

لم يُفحصAD09

الهوية ·A team member is added whose phone belongs to another country

المتوقّع: Allowed, and written to the audit log — decided 2026-08-18. Staff are NOT customers: one admin may hold Kuwait and Bahrain and cannot carry two phones. The customer rule («a Kuwaiti number posts only in Kuwait») stays untouched; this is an explicit, recorded exception for the team, not a loosening of it.

الناتج: not built yet

قراركthe customer rule («a Kuwaiti number posts only in Kuwait») cannot be the staff rule if one person holds two countries

9

لوحة الدول

0 / 11

One page reading six scorecards — the only surface allowed to see across countries.

اللوحة0/9
لم يُفحصM08

The cross-country console is opened

المتوقّع: Six scorecards, no raw listing reads, and it says how old the numbers are

الناتج: not built yet

قرارك«a page to show data/performance for the countries»

لم يُفحصCN01

The console is asked where its numbers come from

المتوقّع: Pre-aggregated daily scorecards — it never queries a country's listings

الناتج: not built yet

قرارك«the cross-country console is the ONLY reader across countries and it reads daily scorecards»

لم يُفحصCN02

A scorecard is stale because a sweep failed

المتوقّع: The staleness is SHOWN — an old number presented as current is worse than a gap

الناتج: not built yet

استنتاجthe same habit as this gate: name what is not fresh

لم يُفحصCN03

A new country is created

المتوقّع: It appears on the console automatically, with empty figures rather than absence

الناتج: not built yet

استنتاجan absent country reads as a broken page

لم يُفحصCN04

An inactive country is viewed

المتوقّع: Still listed, marked inactive — it is being prepared, and preparation needs visibility

الناتج: not built yet

استنتاجfollows CO01

لم يُفحصCN07

The console is loaded with six countries

المتوقّع: Fast — six scorecard reads, not six country sweeps

الناتج: not built yet

استنتاجfollows CN01

لم يُفحصCN09

One aggregate in a country's rollup fails while the rest succeed

المتوقّع: That figure reads «—», never 0. The rollup swallows a failed count into a zero today, so a partial failure writes a scorecard that is structurally valid and numerically a lie.

الناتج: not built yet

استنتاجCN02 covers a scorecard that is STALE; this is one that is FRESH AND WRONG

لم يُفحصCN08

A country that carried live ads and paying dealers is deactivated

المتوقّع: Its scorecard FREEZES at the last real figures and is labelled as of that date — it does not drop to zero.

الناتج: not built yet

استنتاجfollows CO04 and CN02; CN04 covers a country that is inactive because it is being prepared, not one that was switched off

لم يُفحصCN10

The console asks for a country's scorecard on its first day, before any rollup has ever run

المتوقّع: «لم يُقس بعد», not zeros — and the gate names any country whose latest scorecard is older than 48 hours. The WRITE side has an owner: there are no triggers here, so a per-country scorecard exists only if a scheduled sweep writes it.

الناتج: not built yet

استنتاجCN01 to CN07 are seven rows about the reader and none about the writer

لم يُفحصCN05

المال ·Money is shown across countries

المتوقّع: Each in its own currency, never summed

الناتج: not built yet

استنتاجfollows MO11 and MO12

لم يُفحصCN06

الصلاحيات ·An admin who holds only Kuwait opens the console

المتوقّع: A DECISION: is the console owner-only, or does it show only their countries?

الناتج: not built yet

استنتاجit is the one surface that crosses the fence his architecture draws

جولتان لكل سيناريو: واحدة فور انتهاء خطوته، وواحدة كاملة بعد انتهاء كل الخطوات — لأن خطوة متأخرة قد تكسر شيئًا أثبتته خطوة سابقة، ولا يكشف ذلك إلا إعادة التشغيل الكاملة. الخطوة رقم 0 «ما ينكسر» ليست خطوة تنتهي — تُعاد في كل جولة، لأن الموقع الشغّال اليوم هو أكثر شيء معرّض للكسر.
pnpm gcc:gate جولة كاملة · pnpm gcc:gate S1-phones جولة خطوة.

🔧🧪