@appaflytech/zeyrek-ai-mcp 0.0.52 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/knowledge/componentCatalog.js +3 -1
  2. package/dist/knowledge/componentEvents.js +13 -1
  3. package/dist/knowledge/mobileComponentCatalog.js +4 -1
  4. package/dist/knowledge/normalize.js +4 -3
  5. package/dist/platform/argumentAliases.js +13 -4
  6. package/dist/platform/bodyFile.js +195 -38
  7. package/dist/platform/capture/actions.js +24 -5
  8. package/dist/platform/capture/capture.js +80 -11
  9. package/dist/platform/capture/persistent-browser.js +23 -1
  10. package/dist/platform/capture/readiness.js +32 -1
  11. package/dist/platform/errors.js +45 -5
  12. package/dist/platform/http.js +17 -9
  13. package/dist/platform/responses.js +7 -1
  14. package/dist/platform/secrets.js +8 -0
  15. package/dist/platform/token.js +32 -21
  16. package/dist/resources/examples/mobile-offline-list.json +1 -1
  17. package/dist/resources/examples/mobile-offline-queue.json +1 -1
  18. package/dist/resources/examples/mobile-offline-seamless.json +12 -3
  19. package/dist/resources/guides/deployment.md +9 -0
  20. package/dist/resources/guides/microservice.md +13 -1
  21. package/dist/resources/guides/mobile-publish.md +4 -2
  22. package/dist/resources/guides/mobile-steps.md +40 -3
  23. package/dist/resources/guides/mobile.md +38 -8
  24. package/dist/resources/guides/page.md +39 -8
  25. package/dist/resources/guides/translation.md +3 -1
  26. package/dist/resources/schema-fingerprint.json +2 -2
  27. package/dist/tools/applications.js +7 -2
  28. package/dist/tools/deployments.js +1 -1
  29. package/dist/tools/designSchemaPatch.js +7 -2
  30. package/dist/tools/environments.js +40 -0
  31. package/dist/tools/flows.js +30 -3
  32. package/dist/tools/mobile.js +56 -0
  33. package/dist/tools/mobileStore.js +1 -1
  34. package/dist/tools/pages.js +45 -11
  35. package/dist/tools/platformInfo.js +1 -1
  36. package/dist/tools/screenWrite.js +107 -1
  37. package/dist/tools/variables.js +1 -1
  38. package/package.json +1 -1
  39. package/dist/resources/guides/mobile-flow.md +0 -64
@@ -186,6 +186,13 @@ DTO'dan süzülmez (WF-026 — `platform_guide('workflow')` §Hata yolu). Sözle
186
186
  `filters`'a `source: "body"` + `isRequired: false` bir id filtresi; gövdede id **varsa** eşleşen
187
187
  satır güncellenir, **yoksa** yeni satır açılır. `isRequired: true` yaparsan "yeni kayıt" yolu
188
188
  kapanır (id gelmeyince uç reddeder).
189
+ - **İstemcinin ürettiği kimlik korunur** (çevrimdışı not, telefonda üretilen UUID): upsert'te `uuid` PK'nın `eq`
190
+ filtresi eşleşme bulamazsa yeni satır **o kimlikle** açılır (filtre kökte ya da tek `and` grubundaysa; `or` varken ve
191
+ sayı kimlikte taşınmaz — zeyrek-backend incident-log #190). Sonraki eşitleme aynı satırı bulur; ayrı `client_id` kolonu gerekmez. Aynı kural
192
+ workflow table adımının upsert'inde de geçerlidir. `properties.generationType: 1` (ByDefault) PK'ya create'te kimlik
193
+ `fields`'tan da yazılabilir; `0` (Always) PK'ya yazmak `AlwaysGeneratedFieldCantBeWritten` alır
194
+ (`platform_guide('entity')`). Aynı anahtarla aynı anda gelen iki upsert'in ikisi de create yoluna düşebilir; ikincisi
195
+ PK çakışmasıyla hata alır (#191, açık) — çevrimdışı kuyruk kayıtları tek tek gönderir.
189
196
 
190
197
  > **Upsert'te üretilen alan tuzağı.** `operation: 'upsert'` UPDATE yolunda da alan ifadelerini
191
198
  > yeniden değerlendirir; sicil/kod gibi üretilen bir değeri `source: 'expression'` ile yazarsan
@@ -380,7 +387,12 @@ döner, hata vermez. Karmaşık erişim modelinde platform sahip koşulunu satı
380
387
  AND'ler (`platform_guide('access')`); orada bu grup gereksizdir ama zararsızdır. AND/OR filtrenin
381
388
  kendinde değil **bir öncekine bağlantısındadır**: `or` taşıyan çocuk, ilk çocuk bile olsa hedefi
382
389
  genişletir. Kural yalnız kayıtta uygulanır; 2026-10-15'ten önce kaydedilmiş uyumsuz uç yeniden
383
- kaydedilince reddedilir. Workflow table adımı bu kuralı uygulamaz.
390
+ kaydedilince reddedilir. Workflow table adımı bu kuralı uygulamaz; ama grubu korur: `owner = X AND (id = A OR
391
+ client_id = A)` gibi iç içe grup update/delete/upsert'te olduğu gibi gider (2026-10-30'a dek grup düşüyor, çocukları kendi
392
+ bağlacıyla düz listeye katılıyordu ve sahiplik koşulu atlanabiliyordu — zeyrek-backend incident-log #189).
393
+
394
+ **Okumada VE/VEYA gruplaması serbesttir:** `get`/`count` ucunda `filterType: 'group'` + `children` ile
395
+ `durum = 'açık' AND (sahip = ben OR paylaşılan = true)` kurulur; tek kök kuralı yalnız yazan uçlar içindir.
384
396
 
385
397
  ### Toplu güncelleme FİLTREYLE kurulmaz
386
398
 
@@ -129,8 +129,10 @@ açılmaz, yükleme `mobile_asset_create { path: files[i].path }` ile yapılır.
129
129
 
130
130
  ## Hedefler
131
131
 
132
- `build` (iki platform, yalnız derler) · `testflight` (yalnız iOS) · `internal` (yalnız Android, Play
133
- iç test). Android paketi `packageType`: `aab` (varsayılan, mağaza) · `apk` (yalnız `build`; telefona doğrudan
132
+ `build` (iki platform, yalnız derler; iOS'ta imzalama kimliği yokken imzasız **simülatör** `.app.zip`'i — uygulamanın
133
+ kendi derlemesini bilgisayarda denemenin yolu; kimlik tamsa imzalı `.ipa` çıkar ve simülatörde açılmaz, yarım kimlik
134
+ `secrets_missing`) · `testflight` (yalnız iOS) · `internal` (yalnız Android, Play iç test). Sayfa değişikliğini telefonda
135
+ ya da kendi Mac'inde görmek için derleme gerekmez: `platform_guide('mobile')` §Cihazda dene → Senin Mac'inde. Android paketi `packageType`: `aab` (varsayılan, mağaza) · `apk` (yalnız `build`; telefona doğrudan
134
136
  kurulur, mağazaya **gitmez** — indirme bağlantısı Studio'da). Runner verilmezse backend seçer: kurumun kendi
135
137
  Mac'i çevrimiçiyse o, değilse Zeyrek'in runner'ı; yanıtta `runnerKind` (`own` · `zeyrek`). TestFlight'ta runner yükledikten sonra iş `running` + `store_processing` kalır (`awaitingStore`),
136
138
  Apple işleyince `completed` (en çok 60 dk); bu durumda iptal 409. İptal 409'unun mesajı nedeni söyler (iş zaten
@@ -11,10 +11,10 @@ Motorun kendi adımları (`setter`, `condition`, `microservice`, `code`, `functi
11
11
 
12
12
  | Adım | Mobilde |
13
13
  |---|---|
14
- | `redirect` | yığına iter (göreli yol) ya da dışarıda açar (tam adres) |
14
+ | `redirect` | yığına iter (göreli yol) ya da dışarıda açar (tam adres); sekme sayfası hedefi sekmeye geçer, bulunulan sekmenin kökü yığını köke indirir (`platform_guide('mobile')` §Gezinme) |
15
15
  | `navigate` | eski mobil şemaların adı — `redirect`'in takma adıdır; gateway `redirect`'e çevirir |
16
16
  | `alert` | tipli uyarı (`success`/`error`/`info`/`warning`) toast'tur ve beklemez; düz uyarı onay bekler. `message`/`description` metin şablonudur (`"{{ t('kayit.tamam') }}"`; `platform_guide('page')` §alert). Eylem düğmeli bildirim ("Silindi · Geri al") `alert`le kurulmaz: sayfaya bir `toast` bileşeni koyun (`actions: [{ text, flow }]`, gerekirse `keepOpen`), akış setter'la `isOpen: true` yazar; basış eylemin kendi akışını koşar (`actionClick { text, index }`) |
17
- | `confirm` | `yes` / `no` dalları; `message`/`title`/`confirmText`/`cancelText` metin şablonudur (`{{ }}`, çeviri `t('anahtar')`) |
17
+ | `confirm` | `yes` / `no` dalları; `message`/`title`/`confirmText`/`cancelText` metin şablonudur (`{{ }}`, çeviri `t('anahtar')`). `page_screenshot`'ta pencere `awaiting-user`'dır: `preview-dialog__confirm` → `yes`, `preview-dialog__cancel` → `no` (SCR-023) |
18
18
  | `download` | adres cihazın açma sayfasında açılır; ikili gövde (JSON değil) önbelleğe dosya olarak yazılıp paylaşım sayfasında (`expo-sharing`) açılır, Studio mobil önizlemesi dosyayı indirir (`MOB-005`, bilgi) |
19
19
  | `storage` | `args.name` `state.page.<key>` / `state.app.<key>` ise o state'i yazar (`platform_guide('mobile')` §Kalıcı state), değilse cihaz deposuna |
20
20
  | `setcookie` | güvenli cihaz deposuna (SecureStore) yazar; değer sonraki her istekte `X-Flow-Cookie` başlığıyla gider ve middleware/workflow onu web'deki gibi `cookie.<ad>` olarak okur — `Cookie` başlığıyla gitmez, çünkü iOS onu yerel çerez kavanozuyla virgülle birleştirip bozuyordu (`MOB-005`, bilgi); aynı adlı sunucu çerezi varsa o kazanır. Studio paleti mobilde de sunar |
@@ -26,7 +26,7 @@ Motorun kendi adımları (`setter`, `condition`, `microservice`, `code`, `functi
26
26
  | `notification-cancel` | `args.name` kimlikli planlı bildirimi siler (planlamadaki gibi metin şablonu: `'ilac-{{ item.id }}-0800'`), `'*'` hepsini; hep `always` |
27
27
  | `permission` | bir cihaz iznine bakar ya da ister. `args.value` izin türü, **düz metin — ifade olamaz** (mağaza derlemesinin taşıyacağı izinler bu değerden, akış koşmadan hesaplanır; `FLOW-038`); `args.name` kip: `check` yalnız okur (sistem penceresi açılmaz), `request` verilmemiş ve hâlâ sorulabilen izni ister (boş → `request`). Sonuç `Steps.<adım>.data = { status, canAskAgain }`: `granted` · `denied` · `undetermined` · `unavailable` (derleme bu izni taşımıyor ya da web önizlemesi); `canAskAgain: false` → yalnız Ayarlar'dan değişir. `tracking` (iOS App Tracking Transparency) sonucu ayrıca `advertisingId` taşır: izin `granted` iken IDFA (iOS) / AAID (Android), değilse `null`; oynatıcı `tracking`'i hiç taşımaz (`unavailable`). Red hata değildir, hep `always`. Türler: `mobile_store_get { detail: ['permissions'] }` (`support: supported` satırlarının `key`'i) |
28
28
  | `open-settings` | uygulamanın sistem ayar sayfasını açar; `Steps.<adım>.data.opened`. Argüman almaz |
29
- | `back` | gezinme yığınında bir geri (iOS geri kaydırmasıyla aynı sonuç; kök ADR 0013). Argüman almaz (yazılan okunmaz), sonuç yazmaz, hata üretmez, hep `always`. Yığının ilk ekranında hiçbir şey yapmaz; Studio önizlemesinde runtime'ın kendi gezinmesi yoksa yine hiçbir şey, web uygulamasında tarayıcının geri'sidir. Formu kaydedip önceki ekrana dönmek için `redirect` yerine bunu kullanın: `redirect` yığına yeni ekran iter, geri kaydırması kaydedilen forma döner. 2026-10-08 öncesi runtime ve backend `back`'i tanımaz |
29
+ | `back` | gezinme yığınında bir geri (iOS geri kaydırmasıyla aynı sonuç; kök ADR 0013). Argüman almaz (yazılan okunmaz), sonuç yazmaz, hata üretmez, hep `always`. Yığının ilk ekranında hiçbir şey yapmaz ve telefonun defterine `back-unavailable` düşer (`mobile_error_list`); oynatıcıda ve mağaza kabuğunda aynı çalışır (eski mağaza kabuğu sürümlerinde boştu). Studio önizlemesinde runtime'ın kendi gezinmesi yoksa yine hiçbir şey, web uygulamasında tarayıcının geri'sidir. Sekme sayfasına dönmek için `redirect` de olur: hedef sekme sayfasıysa sekmeye geçer, bulunulan sekmenin köküyse yığın köke iner (kopya itmez). Formu kaydedip önceki ekrana dönmek için `redirect` yerine bunu kullanın: `redirect` yığına yeni ekran iter, geri kaydırması kaydedilen forma döner. 2026-10-08 öncesi runtime ve backend `back`'i tanımaz |
30
30
  | `ad-show` | tam ekran reklam (kök ADR 0012). `args.value` mağaza ayarındaki yerleşimin **adı**, düz metin (`FLOW-039`); biçimi `interstitial` · `rewarded` · `rewardedInterstitial` olmalı. Reklam yüklenip gösterilir, kullanıcı kapatınca `Steps.<adım>.data = { shown: true, reward }` ile sürer — `reward` ödüllü reklamı sonuna kadar izleyende `{ type, amount }`, değilse `null` (ödül ayrı dal değil, `condition` ile okunur). 10 sn'de reklam yoksa, SDK hata verirse, derleme reklam taşımıyorsa ya da rıza yoksa `error.status: 'unavailable'`; tanımsız ya da banner yerleşimi `invalid`: `error` dalı → hata akışı → `always`. Oynatıcı ve web önizlemesi her zaman `unavailable` — reklamsız dalı orada sınayın. Paletten yalnız mobilde |
31
31
  | `biometric-auth` | kullanıcıyı Face ID / parmak izi (yedekle cihaz parolası) ile doğrular (kök ADR 0021). `args.title` sistem penceresindeki gerekçe, `args.cancelText` vazgeç düğmesi — ikisi metin şablonu, boşsa uygulama dilindeki varsayılan; `args.value` yedek, düz metin: `device` (biyometri olmazsa cihaz parolası; boşsa bu) ya da `none` (`FLOW-042`; `fallback` / `reason` / `cancelLabel` anahtarı sessizce düşer). Başarı: `Steps.<adım>.data = { verified: true }` → `always`. **Vazgeçmek hatadır**: `error.status` `cancelled` · `not-enrolled` · `lockout` · `failed` · `unavailable` (donanım yok ya da derleme taşımıyor) · `unsupported` (web uygulaması, production `/view`) · `invalid`; `error` dalı → hata akışı → **o da yoksa akış bu adımda biter**, `always`'e bağlı adım koşmaz. Yerel bir kapıdır, sunucu için kanıt değildir. Paletten yalnız mobilde |
32
32
  | `scan-code` | tam ekran kod tarayıcı (kök ADR 0017): tek kod okur ve kapanır. `args.value` izinli türler, **virgüllü düz metin** (`FLOW-042`): `aztec` · `codabar` · `code39` · `code93` · `code128` · `datamatrix` · `ean8` · `ean13` · `itf14` · `pdf417` · `qr` · `upc_e`; boş → hepsi. `upc_a` yok: UPC-A etiketi `ean13` ve on üç haneyle (başında `0`) döner. `args.title` ekrandaki yönerge, metin şablonu (`{{ t('anahtar') }}`). Sonuç `Steps.<adım>.data`: `{ value, type, source }` — `source` `camera` ya da `manual` (ekrandaki "elle gir"; tür bilinmiyorsa `type: null`). `error` dalı: `status` `cancelled` (kullanıcı kapattı — **vazgeçme bir hatadır, `error` dalını bağla**) · `denied` (kamera izni yok) · `unavailable` (derleme kamerayı taşımıyor ya da tarayıcı zaten açık) · `unsupported` (web uygulaması) · `invalid`. Kamera izni kendiliğinden derlemeye girer (adım production'daysa). Önizleme (`/dev`, `page_screenshot`) kamera açmaz, "kod gir" penceresi çizer: `__scan-value`'ya yaz, `__scan-confirm` ya da `__scan-cancel` (çok türde `__scan-type-<tür>`). `page_screenshot` pencereyi yalnız `captureUnready: true` ile sürer (akış pencereyi beklerken "koşuyor" sayılır; her eylem 15 sn bekler). Tam gövde: `ngapps://examples/mobile-scan-code` |
@@ -34,6 +34,43 @@ Motorun kendi adımları (`setter`, `condition`, `microservice`, `code`, `functi
34
34
  | `restore-purchases` | mağaza hesabındaki satın almaları geri getirir (telefon değişti, uygulama yeniden kuruldu); argümansız. Başarı `Steps.<adım>.data = { entitlements }` — geri gelecek bir şey yoksa da başarıdır, sonucu hakka bakarak söyleyin. Hata sözlüğü `purchase`'la aynı ve o da kapıdır. Satın alma ekranında bir "Satın almaları geri yükle" düğmesi beklenir (`MOB-034`); yalnız tüketilen ürün satan ekranda beklenmez, tüketilen ürün geri yüklenmez |
35
35
  | `balance-spend` | tüketilen ürünün bakiyesinden harcar (kredi, jeton; kök ADR 0027 bakiye bölümü). `args.value` bakiyenin **adı**, `args.title` **miktar** — ikisi de metin şablonu: düz `kredi` / `10` ya da `{{ adet.value }}` (`FLOW-046`: boş değer 422; katalogda tüketilen ürünün `balance`'ı olmayan düz ad yazılmaz; `{{ }}` dışındaki ifade harfi harfine metin sayılır; `balance`/`amount` anahtarı sessizce düşer). Harcama sunucuda atomiktir (aynı sahibin eşzamanlı harcaması sıralanır, bakiye aşılamaz). Başarı `Steps.<adım>.data = { balances }` → `always`; `Balances` kendiliğinden güncellenir, ayrıca okumayın. **Kapıdır**: `error.status` `insufficient` (bakiye yetmiyor — negatif bakiye de bu) · `unavailable` (çevrimdışı ya da sunucuya ulaşılamadı; çevrimdışı yazma kuyruğuna GİRMEZ) · `unsupported` (web uygulaması, production `/view`, kabuk satın almayı taşımıyor) · `invalid` (ad ya da miktar geçersiz; miktar 1 ve üstü tam sayı) · `failed`; `error` dalı → hata akışı → **o da yoksa akış bu adımda biter**. Başarılı her tıklama ayrı harcamadır; sonucu belirsiz kalan (`unavailable`) harcamanın anahtarı cihazda durur, aynı bakiye ve miktarla yeniden dokunuş iki kez düşmez. Anahtarı akışın verdiği harcama workflow `balance` adımındadır. Paletten yalnız mobilde |
36
36
 
37
+ ## Sayfa kapanınca
38
+
39
+ Sayfa söküldüğünde (geri, yığının köke inmesi, çıkış — sekme değişimi sayfayı sökmez) açık akış yarıda kalmaz (kök ADR 0030): yeni koşum başlamaz, açık
40
+ koşumlar telefona yazan adımlarla sonuna kadar sürer. 30 sn'de bitmeyen koşum kapatılır.
41
+
42
+ | Adımlar | Sayfa kapalıyken |
43
+ |---|---|
44
+ | `microservice`, `storage`, `setcookie`, `code`, `function`, `condition`, `validation`, `query`, `flow-trigger`, `bpm`, `notification-schedule`, `notification-cancel`, `haptic`, `balance-spend` | koşar |
45
+ | `setter`, `alert`, `confirm`, `navigate`, `redirect`, `back`, `share`, `download` | atlanır; akış `always`'tan (`confirm`'de `no`) sürer |
46
+ | `scan-code`, `location`, `permission`, `notification-permission`, `open-settings`, `ad-show`, `purchase`, `restore-purchases` | `error.status: 'page-unmounted'` ile hata yolu |
47
+ | `biometric-auth` | hata yolu; hata yolu yoksa akış biter (kapı doğrulamasız açılmaz) |
48
+
49
+ Atlanan her adım telefonun defterine `page-unmounted` kaydı düşer (`mobile_error_list`). `state.*` yazımı `storage` ya da
50
+ `code` adımıyla yapılır — `setter` atlanır. "İsteği göndermeden önce ekranı değiştir", "önce yaz sonra çık" gibi geçici
51
+ çözümler gerekmez. Studio mobil önizlemesi ve `/view` de sayfa geçişinde boşaltır; "Dene"nin yeniden başlatması ve
52
+ önizlemenin kapanması akışı keser. Web (Svelte) uygulaması akışı sayfayla birlikte keser.
53
+
54
+ ## Önizlemede çekim hedefleri
55
+
56
+ Bileşenin iç kontrolü `<elementId>__<rol>` ile hedeflenir; rol görünen metinden değil kayıttaki anahtardan türer.
57
+ Mobil roller `component_get` çıktısının mobil notlarındadır (`internalRoles` ve bulunamayan hedefin notu yalnız web
58
+ rollerini sayar):
59
+
60
+ | Kimlik | Ne |
61
+ |---|---|
62
+ | `<id>__press` | tek basışlı denetimin basış yüzeyi (kök kendisi basılabilir değilse `click` buna basar) |
63
+ | `<id>__row-<i>`, `<id>__row-<i>__handle` | liste satırı, sürükleme tutamacı (`swipe` hedefi) |
64
+ | `<id>__chip-<değer>` | çip seçeneği; tablonun filtre çipi `__chip-<kolon>` |
65
+ | `<id>__tab-<anahtar>` | `tabs` bileşeninin sekmesi (`tab` eylemi) |
66
+ | `<id>__day-<yyyy-MM-dd>`, `<id>__prev`, `<id>__next` | `calendar`/`datepicker` günü ve ay oku (`click`) |
67
+ | `__tabs`, `__tabs__item-<sayfa slug'ı>` | kabuğun sekme çubuğu ve öğesi |
68
+ | `preview-dialog__confirm`, `preview-dialog__cancel` | `confirm`/`alert` penceresi (SCR-023) |
69
+ | `preview-biometric__verify`, `preview-biometric__cancel` | `biometric-auth` penceresi (SCR-018) |
70
+ | `__scan-value`, `__scan-type-<tür>`, `__scan-confirm`, `__scan-cancel` | `scan-code` "kod gir" penceresi (SCR-019; önek yok) |
71
+
72
+ Pencere açıkken altındaki sayfa öğesine `click` uygulanmaz (SCR-023).
73
+
37
74
  ## Bildirim tarifi
38
75
 
39
76
  Bildirim adımları paletten yalnız mobil uygulamada sunulur. Tarif — S04 "her gün hatırlat": önce
@@ -17,7 +17,8 @@ anlamları ayrı.
17
17
  1. **Uygulama ve kabuk.** Tek çağrıda: `mobile_app_create` (uygulama + sayfalar + kabuk layout'u +
18
18
  sekme ayarı + ana sayfa bağı, `dryRun` ile önce planı görün). Tekil araçlarla aynı sıra:
19
19
  `application_create({ type: 'mobile' })` → `page_create` × N → `layout_create` (kabuk) →
20
- `page_update({ layoutId })` × N → `application_update({ homepageId, properties.mobile.tabs })`.
20
+ `page_update({ layoutId })` × N → `application_update({ homepageId, properties.mobile.tabs })` (ayar yalnız
21
+ development'a yazılır, production'a dağıtımla geçer — APP-001).
21
22
  Sekme ayarı sayfa **id**'si ister; bu yüzden sayfalardan SONRA yazılır.
22
23
  2. **Veri.** Entity, microservice ve workflow web'dekiyle aynıdır (`platform_guide('microservice')`).
23
24
  Sayfa akışı `microservice` adımıyla aynı uçları çağırır. Veri bağlamanın telefon
@@ -368,7 +369,8 @@ etiketi ailesini `typographyStyle`'dan alır, alan etiketleri platform yazı tip
368
369
 
369
370
  ## Gezinme
370
371
 
371
- - **Yığın varsayılandır; ayar yoktur.** `redirect` adımı hedef sayfayı yığına iter, geri hareketi
372
+ - **Yığın varsayılandır; ayar yoktur.** `redirect` adımı hedef sayfayı yığına iter (sekme sayfası hedefi sekmeye
373
+ geçer, aşağıda), geri hareketi
372
374
  (cihaz, app-bar ya da tarayıcı geri) önceki ekrana döner. Göreli yol uygulama
373
375
  köküne eklenir; tam adres (`https://…`) dışarıda açılır.
374
376
  - **Sekmeler uygulama ayarıdır, layout'ta değil.** `application_update({ properties: { mobile: { tabs } } })`:
@@ -398,6 +400,13 @@ etiketi ailesini `typographyStyle`'dan alır, alan etiketleri platform yazı tip
398
400
  gizlenir; alan yoksa görünür). `true`/`false`'tan başka değer (ör. metin `"false"`) yok sayılır ve `MOB-007` uyarır.
399
401
  İkinci dokunuş (`reselect`) ile sekme değişimi (`keepStackOnSwitch`) ayrı ayardır; eski runtime anahtarları bilmez ve
400
402
  varsayılan davranışta kalır.
403
+ - **Sekme sayfasına `redirect` sekmeye geçer, kopya itmez.** Hedef (`pageId` ya da sayfanın yolu) bir sekme sayfasıysa
404
+ o sekmeye geçilir; bulunulan sekmenin kök sayfasıysa o sekmenin yığını köke iner — "kaydet → listeye dön" böyle
405
+ yazılır. Başka sayfa bulunulan sekmenin yığınına itilir.
406
+ - **`back` adımı iki kabukta da yığında bir geri gider.** Gidemezse (yığının ilk ekranı) hiçbir şey yapmaz, akış yine
407
+ `always`'tan sürer ve telefonun defterine `back-unavailable` düşer (`mobile_error_list`; kök ADR 0013).
408
+ - **Sayfadan çıkılınca açık akış yarıda kalmaz**: telefona yazan adımlar sonuna kadar koşar, ekrana dokunanlar atlanır
409
+ (`platform_guide('mobile-steps')` §Sayfa kapanınca).
401
410
  - **Çekmece bir bileşendir.** `drawer` widget'ı + `app-bar`'ın `menuTarget` prop'u çekmeceyi açar.
402
411
  Çekmece layout'un kök dizisindedir (`views: [shell_root, nav_drawer]`), kabuk kabının içinde değil.
403
412
  - Kabuk örnekleri: `ngapps://examples/mobile-app-shell-stack` (yığın), `ngapps://examples/mobile-app-shell-tab`
@@ -473,10 +482,11 @@ Sunucudan gönderilen bildirim yerel `notification-schedule`'dan ayrıdır. Kana
473
482
 
474
483
  ## Kalıcı state
475
484
 
476
- State varsayılan olarak **bellektedir**: sayfa her açıldığında `variable_create`'te yazılan `value`'dan
477
- başlar, akışın yazdığı değer ekran kapanınca kaybolur — `state.app` dahil: sayfalar arası
478
- taşımak da `storage` ister (38 S-5). Uygulama kapanıp açıldığında kaldığı yerden
479
- sürmesi gereken değer için değişkene saklama türü verin:
485
+ State varsayılan olarak **bellektedir**: `state.page` sayfa her açıldığında `variable_create`'teki `value`'dan başlar,
486
+ ekran kapanınca kaybolur. **Mobil kabukta `state.app` uygulama + ortam başına tek depodur** (kök ADR 0031): bir
487
+ sayfanın yazdığı arkadaki sayfada da anında görünür; açılan sayfanın `value`'su yalnız depoda olmayan anahtarı
488
+ doldurur. Depo süreç ömrü kadar yaşar ve çıkışta silinir. Studio önizlemesi ve web her sayfaya kendi kopyasını verir (38 S-5).
489
+ Uygulama kapanıp açıldığında kaldığı yerden sürmesi gereken değer için değişkene saklama türü verin:
480
490
 
481
491
  | `storage` | Depo | Ne için |
482
492
  |---|---|---|
@@ -496,8 +506,8 @@ variable_create({ applicationId, pageId, variables: [
496
506
  **ilk açılış** değeridir. Saklı değerin biçimi sonradan değiştirdiğiniz `value` ile uyuşmuyorsa saklı
497
507
  olan kazanır; sıfırlamak için anahtarı yeniden adlandırın.
498
508
  - `pageId` verilmeden tanımlanan değişken (`state.app`) uygulama kapsamındadır: her sayfa açılışta aynı
499
- saklı değeri okur. Açık iki sekme birbirinin yazdığını canlı görmez — değer sayfa açılırken okunur.
500
- - Çıkışta (kullanıcının çıkışı ya da oturumun düşmesi) iki depodaki state değerleri silinir.
509
+ saklı değeri okur. Mobil kabukta açık sayfalar birbirinin yazdığını canlı görür; web'de değer sayfa açılırken okunur.
510
+ - Çıkışta (kullanıcının çıkışı ya da oturumun düşmesi) iki depodaki state değerleri ve bellekteki `state.app` silinir.
501
511
  - Ortamlar ayrıdır: `development`'ta saklanan değer `production`'da okunmaz.
502
512
  - `variable_update` `storage` vermezse kayıttaki tür korunur; belleğe çevirmek için `storage: 'memory'`.
503
513
  `value` vermezseniz kayıttaki değer de korunur — yalnız türü değiştirmek için `{ key, storage }` yeter.
@@ -524,6 +534,20 @@ variable_create({ applicationId, pageId, variables: [
524
534
  - `flowRuns: 0` + `requests: 0` bir tıklamada akışın koşmadığını söyler: olay bağını
525
535
  `page_runtime_get`, akışı `flow_get` ile doğrulayın.
526
536
  - Kuru doğrulama: `page_schema_validate` mobil kataloğa karşı doğrular (`MOB-003`).
537
+ - **Telefonda düşen hata** `mobile_error_list({ applicationId })` ile okunur (NTF-006): kabuk akış hatalarını sunucuya
538
+ gönderir, uçak kipindekiler ağ dönünce gelir. `page-unmounted` (sayfa kapanınca atlanan adım) ve
539
+ `back-unavailable` (geri gidilemedi) kayıtları da buradadır; önizlemede `page_screenshot` `runtimeErrors`'ta görünür.
540
+ - **Önizlemede ağsızlık yalnız hata yolunu sınar** (SCR-020…022): `{ action: 'network', value: 'offline' }` ağı keser,
541
+ `'online'` açar (`actionsReport.online`). Önizlemede önbellek ve yazma kuyruğu YOKTUR: okuma da yazma da `error`
542
+ dalından gider (`queued`, `offlineQueueChange` koşmaz, `error.network` yazılmaz). Kuyruklu senaryo (kaydet → listede
543
+ bekleyen → eşitleme) yalnız cihazda (uçak kipi). İki çağrıya yayılan senaryo `session` ile kurulur — çerez,
544
+ localStorage (`storage: 'device'` state) ve IndexedDB taşınır; `secure` (sessionStorage) ve ağ taşınmaz. Klavye açıkken
545
+ yazılan hâl için `set` + `blur: false` (`changed` koşmaz). Önizleme her çağrıda tek sayfa açar.
546
+ - **Onaylı akış sürülür** (SCR-023): `confirm`/`alert` adımı önizlemede pencere açar; onu açan `click`'in raporu
547
+ `pageState: 'awaiting-user'` der, sonraki eylem `{ action: 'click', target: 'preview-dialog__confirm' }` (onay dalı)
548
+ ya da `'preview-dialog__cancel'` (red dalı; uyarıda yok). Geçici onaysız sürüm yazmaya gerek yoktur.
549
+ - **Çekim hedef kimlikleri** (`__press`, `__row-<i>`, `__chip-<değer>`, `__tab-<anahtar>`, önizleme pencereleri) tek
550
+ tabloda: `platform_guide('mobile-steps')` §Önizlemede çekim hedefleri.
527
551
  - Kare **donuktur**: bağlam "hareketi azalt" ile açılır, bileşen hareketi (giriş, döngü, geçiş, iskelet parıltısı,
528
552
  kabuğun yükleme göstergesi) durur, aynı sayfanın iki karesi aynı çıkar. Hareketin kendisini görmek için
529
553
  `motion: 'play'` (kare çekildiği ana bağlıdır; yanıt `motionNote` taşır).
@@ -547,6 +571,8 @@ Mobile App Store'dan ya da Google Play'den yüklenir (pencere mağaza bağlantı
547
571
  çıkan kod okutulur (120 sn geçerli). **Senin Mac'inde:** kullanıcı kendi Mac'ini bir kez ekler, simülatör o Mac'te
548
572
  açılır; Mac'i olmayan için Zeyrek'in Mac'inde açma henüz yoktur. **Kaydettiğinde güncellenir:** yazdığın yama
549
573
  telefonda ve simülatörde yenilenir. Ajan kod üretmez, önizleme başlatmaz; kullanıcıyı bu pencereye yönlendirir.
574
+ Uygulamanın kendi derlemesi ayrı yoldur: iOS `target: 'build'` imzalama kimliği yokken simülatör `.app`'i,
575
+ kimlik tamsa simülatörde açılmayan `.ipa` üretir (`platform_guide('mobile-publish')` §Hedefler).
550
576
 
551
577
  ## Yayın
552
578
 
@@ -568,5 +594,9 @@ yaptıkları. Derlenen uygulama
568
594
  - Dar ekran kırılımı (`hideOn: 'mobile'`) mobil uygulamada anlamsızdır: ekran zaten tek boydur.
569
595
  - Ağ yokken ekranlar ve (açıksa) son okunan veri cihazdaki kopyadan gelir: `properties.mobile.offline`
570
596
  (MOB-033), `Steps.<ad>.cache.at`, `error.network` — `platform_guide('page')` §Kural 1g-4. Önizleme önbellek kullanmaz.
597
+ Yalnız GET okuma saklanır (POST okuma ağsız açılmaz → GET + query-string, FLOW-044); kuyruk değişimi sayfanın
598
+ `offlineQueueChange` olayıdır, yoklama gerekmez (§Kural 1g-4).
599
+ Ayar (`properties`, ana sayfa, bulunamadı sayfası) `application_update` ile yalnız **development**'a yazılır; production'da
600
+ `deployment_start`'tan sonra geçerli olur (APP-001) — önce development'ta deneyin.
571
601
  "Uygulama ağsızken de çalışsın" isteğinin tarifi (okuma GET, yazma kuyruğu, liste birleştirme, düğme kilidi, korumasız
572
602
  açılış, bant kapalı): §Kural 1g-4 "Ağsızken fark göstermeyen uygulama", örnek `ngapps://examples/mobile-offline-seamless`.
@@ -1537,7 +1537,9 @@ Kayıtlar kullanıcıya, ortama ve veri diline bağlıdır; çıkışta ve kulla
1537
1537
  yazıp yazmadığını bildirimi veren söyler. Okuyan uca (tablo `get`/`count`, görünüm kaynağı) verilirse **MS-025** uyarır.
1538
1538
  - **Akış:** `microservice` adımına `queued` dalı (yalnız mobil). Ağ hatasında (yanıt hiç yok) dal bağlıysa, yöntem
1539
1539
  yazansa (POST/PUT/DELETE) ve dosya parametresi yoksa kayıt kuyruğa girer ve `queued` koşar; `Steps.<ad>.data` `null`,
1540
- `Steps.<ad>.queued = { clientRequestId, at }`. Dal bağlı değilse adım bugünkü gibi `error`'a gider; dosya
1540
+ `Steps.<ad>.queued = { clientRequestId, at }`. Barındırıcı ağın olmadığını biliyorsa (`Device.isOnline === false`)
1541
+ bu koşullardaki istek hiç denenmez, doğrudan kuyruğa girer (kök ADR 0029) — uçak kipinde `queued` beklemeden koşar;
1542
+ durum bilinmiyorsa istek denenir. Dal bağlı değilse adım bugünkü gibi `error`'a gider; dosya
1541
1543
  parametresinde `Steps.<ad>.error.code = 'fileNotQueued'`. HTTP hatası — önündeki vekil sunucunun 502/503'ü dahil —
1542
1544
  kuyruğa girmez. Web uygulamasında ya da bildirimsiz servise bağlanan `queued` için yazım **FLOW-043** uyarısı alır.
1543
1545
  - **Sunucu karar verir:** cihaz servisin bildirimini bilmez. `queued` dalı bağlı yazmanın İLK isteği de
@@ -1554,8 +1556,14 @@ Kayıtlar kullanıcıya, ortama ve veri diline bağlıdır; çıkışta ve kulla
1554
1556
  - **Görünürlük:** kaydın sonucu akışa dönmez. `Device.offline.queue = { pending, rejected }` ve
1555
1557
  `Device.offline.rejected` (`id`, `service`, `at`, `status`, `code`, `message`); kod adımında `Page.offline.list()`,
1556
1558
  `.count()`, `.sync()`, `.resolve(id, 'retry' | 'discard')`. Süresi dolan kayıt yeniden denenmez.
1557
- - **Önizleme:** Studio ve web'de kuyruk yoktur, `queued` hiç koşmaz; `page_screenshot` ağı kesemez. Açık ekran ilk
1558
- çizildiği kopyayla kalır — akışa eklenen dal ekran bir sonraki açılışta etkindir.
1559
+ - **Kuyruk değişince ekran tazelenir:** mobil sayfanın `offlineQueueChange` olayı (`page.events`, `mount`/`destroy`/`focus`
1560
+ yanında; Studio'da "Çevrimdışı kuyruk değiştiğinde") kayıt girdiğinde, gönderildiğinde, reddedildiğinde ve silindiğinde koşar.
1561
+ Yük taşımaz: güncel sayaç olaydan hemen önce yazılan `Device.offline`'dadır. İlk okuma olay üretmez; olaya bağlı akış
1562
+ koşarken kuyruğun değişmesi olayı yeniden tetiklemez (kuyruğa yazan akış döngüye girmez). Listeyi bekleyenlerle
1563
+ yeniden kuran yükleme akışını bu olaya da bağlayın — yoklama (`setInterval`, birkaç saniyede bir okuma) gerekmez.
1564
+ - **Önizleme:** Studio ve web'de kuyruk yoktur, `queued` ve `offlineQueueChange` hiç koşmaz. `page_screenshot`
1565
+ `{ action: 'network', value: 'offline' }` ile ağı keser (SCR-020) ama önbellek ve kuyruk olmadığı için okuma da yazma
1566
+ da hata yolundan gider. Açık ekran ilk çizildiği kopyayla kalır — akışa eklenen dal ekran bir sonraki açılışta etkindir.
1559
1567
 
1560
1568
  Örnek: `ngapps://examples/mobile-offline-queue` (doz işaretleme: servis bildirimi + `queued` dalı).
1561
1569
 
@@ -1577,8 +1585,8 @@ Kayıtlar kullanıcıya, ortama ve veri diline bağlıdır; çıkışta ve kulla
1577
1585
  hiç açılmamış sayfa ağ yokken gelmez.
1578
1586
 
1579
1587
  **Doğrulama sınırı:** önizleme (`/dev`, Studio kanvası) ve `page_screenshot` önbellek KULLANMAZ — ayar açık olsa da her
1580
- okuma ağdan gider, `cache` ve `cachedAt` hiç yazılmaz. `page_screenshot` ağı kesemez; çevrimdışı davranış yalnız
1581
- cihazda görülür (uçak modu). Ölçüldü 2026-10-20 (simülatör, mağaza kabuğu): GET servis okuması ağ varken 149 ms'de
1588
+ okuma ağdan gider, `cache` ve `cachedAt` hiç yazılmaz. `page_screenshot`'ın `network` eylemi ağı keser ama önbellek
1589
+ olmadığından okuma hata yolundan döner; önbellekli ve kuyruklu davranış yalnız cihazda görülür (uçak modu). Ölçüldü 2026-10-20 (simülatör, mağaza kabuğu): GET servis okuması ağ varken 149 ms'de
1582
1590
  ağdan, sunucu yanıt vermezken 8,03 sn'de kesilip kayıttan döndü.
1583
1591
 
1584
1592
  Örnek: `ngapps://examples/mobile-offline-list` (liste + "son güncelleme" satırı + bağlantı yok uyarısı). Web
@@ -1602,7 +1610,11 @@ AYNI; veri bağlantı gelince Zeyrek'e kendiliğinden gider. Altı adım birlikt
1602
1610
  `Page.offline.resolve(id, 'retry' | 'discard')` ile karar verdirin; göstermemek bilinçli bir ödünleşimdir.
1603
1611
  4. **Liste bekleyenleri gösterir:** yükleme akışı sunucunun yanıtına `await Page.offline.list()`'teki
1604
1612
  `status === 'pending'` kayıtları katar (`service` yazma ucunun slug'ı, alan değeri `parameters[].value`'dan); ağ
1605
- yokluğu hata metni göstermez (`Steps.<ad>.error && !Steps.<ad>.error.network`).
1613
+ yokluğu hata metni göstermez (`Steps.<ad>.error && !Steps.<ad>.error.network`). Aynı akış sayfanın
1614
+ `offlineQueueChange` olayına da bağlanır: kuyruğa giren kayıt listeye hemen, gönderilen kayıt sunucudan gelir.
1615
+ Kaydı yazan sayfadan çıkılsa da yazma akışı tamamlanır (kök ADR 0030) ve `state.app` mobil kabukta sayfalar arası
1616
+ ortaktır (kök ADR 0031): "istekten önce yaz", "kuyruktan oku" ve "birkaç saniyede bir yokla" geçici çözümleri
1617
+ gerekmez (Defter saha raporu, 2026-10-30).
1606
1618
  5. **Düğme kilidi:** yazan düğme istek süresince `isLoading` + `isDisabled`; her dalın sonunda açılır. Kilitsiz düğmede
1607
1619
  hızlı ikinci dokunuş yeni bir istek ve yeni bir `Idempotency-Key`'dir — sunucu ikisini de uygular (ölçüm: 1,6 sn'de
1608
1620
  üç dokunuş, üç kayıt).
@@ -1613,8 +1625,9 @@ AYNI; veri bağlantı gelince Zeyrek'e kendiliğinden gider. Altı adım birlikt
1613
1625
  `refresh` olayı yükleme akışına bağlıdır — bağlantı dönünce açık ekranın mount akışı kendiliğinden koşmaz (kök ADR
1614
1626
  0025 taslak). İki karar kapanınca bu adım sadeleşir.
1615
1627
 
1616
- Fark yalnız üç yerde kalır: giriş (ağ ister), hiç açılmamış ekran (cihazda kopyası yok) ve bağlantı dönünce gönderilen
1617
- kayıt — artık bekleyen değildir, önbellekteki eski listede de yoktur; liste aşağı çekilene kadar görünmez (ADR 0025). Önizleme ve
1628
+ Fark yalnız iki yerde kalır: giriş (ağ ister) ve hiç açılmamış ekran (cihazda kopyası yok). Yükleme akışı sayfanın
1629
+ `offlineQueueChange` olayına bağlıysa (4. adım) bağlantı dönünce gönderilen kayıt listeden düşmez: gönderim kuyruğu
1630
+ değiştirir, akış yeniden koşar ve kayıt sunucudan gelir (kök ADR 0029). Bağ yoksa kayıt liste aşağı çekilene kadar görünmez. Önizleme ve
1618
1631
  `page_screenshot` önbellek ve kuyruk kullanmaz — davranış yalnız cihazda görülür. Tam gövde (iki ekran, beş akış, iki
1619
1632
  uç, ayar ve değişken): `ngapps://examples/mobile-offline-seamless`.
1620
1633
 
@@ -2035,6 +2048,24 @@ page_schema_patch(ops:[{ op:'set_event', target:'button_1', event:'click', event
2035
2048
 
2036
2049
  `flow_create`'e `flowId` verirsen id'yi önceden bilir, iki adımı tek planda kurabilirsin.
2037
2050
 
2051
+ ### Uzun `code` gövdesi ayrı dosyada: `bodyFile` + `$file`
2052
+
2053
+ Kodu JSON dizgisine kaçışlayarak yazma — bir ters tırnak ya da tırnak hatası adımı bozar. Adımları
2054
+ `bodyFile` ile ver; kod alanına `{ "$file": "<yol>" }` yaz, gateway o dosyanın **metnini** oraya koyar
2055
+ (kod çalıştırılmaz, satır sonu korunur, sonuç satır içi gövdeyle aynı doğrulamadan geçer):
2056
+
2057
+ ```jsonc
2058
+ // ./akislar/kaydet.steps.json → flow_create(host:'page', hostId, name, slug, bodyFile:'./akislar/kaydet.steps.json')
2059
+ [ { "stepId": "yaz", "name": "yaz", "type": "code", "kind": "normal",
2060
+ "args": { "value": { "$file": "./kaydet.js" } },
2061
+ "branches": [{ "id": "<GUID>", "name": "always", "nextStepId": null }] } ]
2062
+ ```
2063
+
2064
+ `./kaydet.js` düz JS'dir. Yol bodyFile'ın klasörüne göre çözülür; göreli `bodyFile` `MCP_INPUT_DIR`'e
2065
+ (yoksa gateway'in çalışma dizinine) göre. Sınırlar: `.js .mjs .ts .txt .sql .html .css`, UTF-8, dosya
2066
+ başına 1 MB, toplam 5 MB, en çok 64 ekleme; eklenen dosyanın içindeki `$file` işlenmez. Yanıtın
2067
+ `bodyFile.included` alanı neyin nereye konduğunu söyler.
2068
+
2038
2069
  ### Sayfada bağlı akış varken YENİ akış eklenemez — ve çözümü
2039
2070
 
2040
2071
  Bir sayfanın herhangi bir olayı zaten bir akışa bağlıysa, o sayfaya ikinci bir `flow_create`
@@ -137,6 +137,7 @@ tek dilli uygulamada da geçerlidir: yazı düzeltilince aynı kırılma olur. *
137
137
  | `colorMap: [{ value: 'Aktif', color: 'success' }]` | `value` kod, görünen yazı `label`'da (çeviri sarmalı olabilir) |
138
138
  | `currentItem.title === 'Dokümanlar'` (breadcrumb tıklaması) | Öğeye ek alan: `{ key: 'klasor', title }` → `currentItem.key === 'klasor'` |
139
139
  | Sekmeyi ya da hata satırını yazısıyla tanımak | Sekme `key`'i / kimliği (`<tabs>__tab-<key>`), bağlamdaki kod |
140
+ | Yer tutucu metni kaydedip ona bakmak: `title === 'Başlıksız not'` (boş başlıklı notu ayırmak) | Ham alanı boş bırak, karar ham değerde: `!note.title`; gösterim ifadede `{{ note.title \|\| t('note.untitled') }}`. Yer tutucuyu kayda yazmak hem dili kayda gömer hem kararı metne bağlar |
140
141
 
141
142
  Gateway akış, sayfa, layout ve widget yazımında denetler (`flow_*`, `page_schema_patch` / `_validate`, `page_create`,
142
143
  `page_update`, `layout_*`, `component_schema_patch`, `component_create` / `_update`):
@@ -153,7 +154,8 @@ Gateway akış, sayfa, layout ve widget yazımında denetler (`flow_*`, `page_sc
153
154
  - **TR-006 (warn):** tek kelimelik metin (`'Aktif'`, `'Evet'`, `'Anasayfa'`) biçimden koddan ayrılmaz; projenin varsayılan
154
155
  dilindeki sözlük değeriyle ya da sayfanın yazısıyla aynıysa uyarır. Bir kod değeriyse yok sayın.
155
156
  - Kaçış: sunucu yalnız metin döndürüyor ve karşılaştırılacak kod yoksa aynı çağrıya `policyOverride: true` — bulgu
156
- yanıtta uyarı olarak kalır. Kalıcı çözüm sunucudan kod istemektir (`platform_gap_report`).
157
+ yanıtta uyarı olarak kalır. Kalıcı çözüm sunucudan kod istemektir (`platform_gap_report`). Karar kendi verinizdeyse
158
+ (yer tutucu, durum adı) kaçış yerine tablodaki doğru şekle çevirin: kaçış yalnız denetimi susturur, kırılma sürer.
157
159
 
158
160
  Denetim sözdizimseldir: kodun içindeki karşılaştırmayı arar, etiket ve mesaj gibi düz metni kanca saymaz. Sunucu tarafı
159
161
  (workflow, microservice, Studio fonksiyonu) denetlenmez — orada da aynı kural geçerlidir.
@@ -1,6 +1,6 @@
1
1
  {
2
- "hash": "b073fadf0690bedd0b74c8c0610e0b843cd7487f3e2c7e9e27361443c52c06be",
3
- "generatedAt": "2026-11-01T20:55:00.329Z",
2
+ "hash": "162e8c2d14c5a0737d023b55790bab139812a00ec8e2e6335ed51981bb27e548",
3
+ "generatedAt": "2026-11-02T08:52:07.624Z",
4
4
  "types": 377,
5
5
  "files": 196,
6
6
  "note": "Şemaları üreten backend kaynaklarının parmak izi. Elle düzenlemeyin: 'npm run generate-schemas' yazar, 'npm run schema:check' denetler.",
@@ -113,7 +113,7 @@ export function registerApplicationTools(server, platform) {
113
113
  }));
114
114
  server.registerTool('application_update', {
115
115
  title: 'Application güncelle',
116
- description: `Application bilgilerini günceller; homepageId zorunludur (mevcut değeri application_get ile alın). ${templateSubscriptionLock}`,
116
+ description: `Application bilgilerini günceller; homepageId zorunludur (mevcut değeri application_get ile alın). ORTAM (K5, b-15): ayar alanları — properties (mobile.offline, sekmeler, geçiş…), homepageId, notFoundPageId — yalnız development'a yazılır; production onları deployment_start ile alır (dal ortamları açıldıkları andaki kopyayı tutar), yani ayarı önce development'ta deneyin. Kimlik alanları (name, slug, color, dns) bütün ortamlara hemen gider. Yanıttaki environmentNote bunu çağrı başına söyler. ${templateSubscriptionLock}`,
117
117
  inputSchema: {
118
118
  id: guid,
119
119
  slug: z.string().min(1),
@@ -132,7 +132,12 @@ export function registerApplicationTools(server, platform) {
132
132
  : [];
133
133
  const response = await platform.org('PUT', `/applications/${id}`, { body });
134
134
  forgetApplication(platform, id);
135
- return withPlatformWarnings(response, warnings);
135
+ // Uç tam kaydı yazar: verilmeyen properties/notFoundPageId development'ta da boşalır (APP-001, inceleme #4).
136
+ const missing = ['properties', 'notFoundPageId'].filter((field) => body[field] === undefined);
137
+ return {
138
+ ...withPlatformWarnings(response, warnings),
139
+ environmentNote: `properties, homepageId ve notFoundPageId verildiği gibi yalnız development'a (ve Core'a) yazıldı; production onları deployment_start ile alır, dal ortamları hiç almaz (APP-001). name/slug/color/dns bütün ortamlara gitti.${missing.length > 0 ? ` ${missing.join(' ve ')} verilmedi: uç tam kaydı yazar, development'taki değer boşaltıldı — korumak için application_get'teki mevcut değeri gönderin.` : ''}`
140
+ };
136
141
  }));
137
142
  server.registerTool('application_delete', {
138
143
  title: 'Application sil',
@@ -51,7 +51,7 @@ export function registerDeploymentTools(server, platform) {
51
51
  }));
52
52
  server.registerTool('deployment_status', {
53
53
  title: 'Deployment durumu',
54
- description: "Projenin son deployment durumunu döner. 'projectId' ZORUNLUDUR (project_list/project_get'ten alın) — verilmezse çağrı MCP şema katmanında reddedilir, platforma hiç gitmez. Hiç deployment yapılmamış projede yanıt BOŞ gelir; o durumda tool 'hiç deployment kaydı yok' bilgisini açıkça döner, yani boş yanıtı 'tool çalışmadı' diye okumayın. Durum satırı modül başına ayrılmaz: süreç tanımları ve iş takvimleri sayfa ve workflow'lardan sonra aynı saga adımında aktarılır; hedefte çalışamayan (eksik workflow/entity/rol) tanım aktarımı DefinitionInvalid ile düşürür. type 'error' ise 'error' alanı düşüş nedenini taşır (DefinitionInvalid'de bulgular 'yol: mesaj | …' olarak eklenir); aktarım atomiktir, hedef değişmemiştir. type 'completed-with-mismatch': deploy tamamlandı, kayıtlar production'a yazıldı, ama ardından koşan özet denetimi production'da fark buldu; sonraki deploy otomatik tam (full) koşar. Yanıt mode (full | delta: deploy'un gerçekte koştuğu kip) ve counters (delta'da modül başına added/updated/deleted) taşır.",
54
+ description: "Projenin son deployment durumunu döner. Bittikten sonra iki ortamın tanım farkını environment_diff verir (özetten; tablo satırları hariç). 'projectId' ZORUNLUDUR (project_list/project_get'ten alın) — verilmezse çağrı MCP şema katmanında reddedilir, platforma hiç gitmez. Hiç deployment yapılmamış projede yanıt BOŞ gelir; o durumda tool 'hiç deployment kaydı yok' bilgisini açıkça döner, yani boş yanıtı 'tool çalışmadı' diye okumayın. Durum satırı modül başına ayrılmaz: süreç tanımları ve iş takvimleri sayfa ve workflow'lardan sonra aynı saga adımında aktarılır; hedefte çalışamayan (eksik workflow/entity/rol) tanım aktarımı DefinitionInvalid ile düşürür. type 'error' ise 'error' alanı düşüş nedenini taşır (DefinitionInvalid'de bulgular 'yol: mesaj | …' olarak eklenir); aktarım atomiktir, hedef değişmemiştir. type 'completed-with-mismatch': deploy tamamlandı, kayıtlar production'a yazıldı, ama ardından koşan özet denetimi production'da fark buldu; sonraki deploy otomatik tam (full) koşar. Yanıt mode (full | delta: deploy'un gerçekte koştuğu kip) ve counters (delta'da modül başına added/updated/deleted) taşır.",
55
55
  inputSchema: { projectId: guid },
56
56
  annotations: readOnly
57
57
  }, handle(config, async ({ projectId }) => {
@@ -2,7 +2,7 @@ import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEvent
2
2
  import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, removeNodeFromTree, planEventRemovals, callerSlots, planEmulatedOps, recordsWithEventRemovals, serverOpsInOrder, assertOpBodies, applySkeletonOps, reshapesSkeleton } from './schemaOps.js';
3
3
  import { guid, projectParam, environmentParam, updates, microOps } from './common.js';
4
4
  import { fetchHost, hostPath } from './screens.js';
5
- import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
5
+ import { assertScreenWriteGuards, screenCatalogNotes, unknownPropOps } from './screenWrite.js';
6
6
  import { emptyWriteDiff, schemaOpDrops, withWriteDiff } from '../platform/writeDiff.js';
7
7
  import { fetchComponentEventNames } from './components.js';
8
8
  import { boxSectionNote, collectIds, ensureBoxSections, expandSectionToggles } from '../knowledge/boxSections.js';
@@ -10,7 +10,7 @@ import { PlatformApiError } from '../platform/errors.js';
10
10
  import { connectDataOpIssues, connectedPropKeys } from '../knowledge/connectDataDefinition.js';
11
11
  import { handle } from '../platform/responses.js';
12
12
  import { slotWarning } from './layouts.js';
13
- import { batchEnvelopeNote, openBatchEnvelopes } from '../knowledge/platform/batchOps.js';
13
+ import { batchEnvelopeNote, batchIndexHint, openBatchEnvelopes } from '../knowledge/platform/batchOps.js';
14
14
  import { applicationTypeField, resolveProfile } from '../knowledge/platform/resolve.js';
15
15
  import { boundFlowSteps, layoutPatchWarnings, screenProfileWarnings } from './profileWrite.js';
16
16
  import { enforceTextHooks, textHookOverrideParam } from './textHookGuard.js';
@@ -87,6 +87,11 @@ export function registerDesignSchemaPatchTool(server, platform, host) {
87
87
  const events = host === 'layout' ? record.events : undefined;
88
88
  const normalized = normalizeSchemaOps(opRecords);
89
89
  assertOpBodies(opRecords, propTypeOps);
90
+ // g-21: tanınmayan prop sayfa yamasındaki gibi istek gitmeden op'uyla adlandırılır; yama bütün reddedilir.
91
+ const unknownProps = await unknownPropOps(platform, project, environment, applicationId, async () => nodes, allOps);
92
+ if (unknownProps.length > 0) {
93
+ throw new PlatformApiError(`${spec.tool}: tanınmayan prop (unknown-prop) — yama atomik olduğu için HİÇBİR op gönderilmedi (istek gönderilmedi): ${unknownProps.map(issue => issue.message).join(' | ')}${batchIndexHint(opened)}`, 400);
94
+ }
90
95
  // TR-005 / TR-006: yamanın yükü op'ların EZDİĞİ kayıtlı değerle karşılaştırılır — orada zaten duran kanca eskidir.
91
96
  const textFindings = await enforceTextHooks(config, {
92
97
  platform, project, environment, applicationId,
@@ -18,6 +18,46 @@ export function registerEnvironmentTools(server, platform) {
18
18
  inputSchema: { projectId: guid },
19
19
  annotations: readOnly
20
20
  }, handle(config, ({ projectId }) => platform.org('GET', `/environments/${projectId}`)));
21
+ server.registerTool('environment_diff', {
22
+ title: 'İki ortamın tanım farkı',
23
+ description: "İki ortamın (varsayılan development → production) TANIMLARINI karşılaştırır ve farklı olanları modül modül listeler: fark türü onlyInSource (yalnız kaynakta, dağıtılmamış) · onlyInTarget (yalnız hedefte) · changed (içerik farklı). Dağıtımdan sonra (deployment_status bitti dedikten sonra) 'production development'la aynı mı' sorusunun tek çağrılık cevabıdır. Fark tanım özetinden (definition digest) gelir, gövdeden değil: sırasız diziler ve çoğu üretilmiş alt kimlik karşılaştırmaya girmez — AMA akış adım parametrelerinin kimlikleri girer: development'ta yeniden yazılan (flow_update) bir akış içeriği aynı olsa da changed görünebilir; dağıtım kimlikleri koruduğu için dağıtımdan hemen sonraki karşılaştırma bundan etkilenmez. Kapsamı özetlenen modüllerdir (uygulama, rol, sorgu, DTO, görünüm, workflow, servis, klasör, layout, sayfa, akış, değişken, fonksiyon, mikrofonksiyon, çeviri); tablolar, bileşenler, erişim politikaları, süreçler, dosyalar ve asistan KAPSANMAZ — yanıtın notCovered alanı bu modülleri sayar, 'eşit' cevabı onlar için değildir. Tablo SATIRLARI (recordTransferMode) karşılaştırılmaz. En çok 50 kayıt satırı döner; fazlası truncated: true. Salt okunur; hiçbir ortama yazmaz.",
24
+ inputSchema: {
25
+ projectId: guid,
26
+ source: z.string().min(1).optional().describe("Kaynak ortam adı; varsayılan 'development'"),
27
+ target: z.string().min(1).optional().describe("Hedef ortam adı; varsayılan 'production'"),
28
+ modules: z.array(z.string().min(1)).max(20).optional().describe("Yalnız bu modüller, backend'in modül adıyla (yanıtın modules[].module değerleri; ör. 'page', 'flow', 'microservice', 'data_transfer_object', 'global-function')")
29
+ },
30
+ annotations: readOnly
31
+ }, handle(config, async ({ projectId, source = 'development', target = 'production', modules }) => {
32
+ // Tek modül ve 50 satır sınırı sunucuda uygulanır (adlar yalnız listelenen kayıtlar için okunur); birden çok modül
33
+ // süzgecinde sunucu tavanı istenir, süzme burada yapılır.
34
+ const single = modules?.length === 1 ? modules[0] : undefined;
35
+ const response = await platform.org('GET', `/revisions/${projectId}/environment-diff`, {
36
+ query: { source, target, ...(single ? { module: single } : {}), limit: modules && modules.length > 1 ? 1000 : 50 }
37
+ });
38
+ const diff = (response?.data ?? response ?? {});
39
+ const wanted = (module) => modules === undefined || modules.includes(module);
40
+ const records = (diff.records ?? []).filter((record) => wanted(record.module));
41
+ const total = modules && modules.length > 1 ? records.length : Math.max(diff.total ?? 0, records.length);
42
+ const moduleCounts = (diff.modules ?? []).filter((module) => wanted(module.module));
43
+ return {
44
+ source,
45
+ target,
46
+ equal: total === 0,
47
+ ...(total === 0 ? { note: `${source} ve ${target} özetlenen modüllerde eşit — notCovered'daki modüller karşılaştırılmadı.` } : {}),
48
+ modules: moduleCounts,
49
+ records: records.slice(0, 50).map((record) => ({
50
+ module: record.module,
51
+ ...(record.name ? { name: record.name } : {}),
52
+ id: record.recordId,
53
+ change: record.state === 'different' ? 'changed' : record.state
54
+ })),
55
+ ...(total > 50 || diff.truncated === true ? { truncated: true, total, truncatedNote: "50 satırdan fazla fark var — modules ile süzün (ör. modules: ['page'])." } : {}),
56
+ notCovered: diff.notCovered ?? [],
57
+ recordsNote: 'Tablo satırları (recordTransferMode) karşılaştırılmaz; bu yalnız tanım farkıdır.',
58
+ ...(diff.digestCheck?.possiblyStale ? { digestNote: 'Ortamlardan birinin özeti hiç denetlenmemiş, son denetimi düşmüş ya da güvenilir değil: fark bayat olabilir.' } : {})
59
+ };
60
+ }));
21
61
  server.registerTool('environment_create', {
22
62
  title: 'Dal environment\'ı aç',
23
63
  description: `Projede yeni bir dal environment'ı açar: kendi PG şeması kurulur ve development'ın tanımları oraya kopyalanır. Asenkron bir saga'dır — yanıt 202 döner, dal hemen kullanılabilir DEĞİLDİR. includeRecords true ise projenin KENDİ veritabanındaki bütün veri tablolarının satırları da kopyalanır (çok dilli companion tablolar ve junction'lar dahil; files/folders ve audit trail tabloları hariç, dış datasource'lara dokunulmaz) — entity'de kayıt taşıması ayarı açık olmasa bile. Merge'de yalnız kayıt taşıması ayarı açık tabloların satırları üç yollu geri taşınır (rehber: merge, Tablo kayıtları). ${templateSubscriptionLock}`,
@@ -64,6 +64,23 @@ function hostNodes(record) {
64
64
  return record.schema ?? record.views ?? [];
65
65
  }
66
66
  const BINDINGS_UNAVAILABLE = 'Barındırıcı kaydı okunamadığı için olay bağları hesaplanamadı; akış listesi yine de günceldir.';
67
+ /**
68
+ * g-21 (D-21, plans/defter-saha-bulgulari): `flow_get` `host` olmadan da çalışır. Akış kimliği sahibini söylemez ve
69
+ * platformda kimlikten sahibe giden uç yoktur; kimlik sırayla sayfa → layout → komponent kaydı olarak okunur. Yalnız
70
+ * 404 sonrakine geçer — başka hata (yetki, 500) sahibi hakkında bir şey söylemez ve olduğu gibi döner.
71
+ */
72
+ async function resolveFlowHost(platform, project, environment, applicationId, hostId) {
73
+ for (const host of ['page', 'layout', 'component']) {
74
+ try {
75
+ return { host, record: await fetchHost(platform, host, project, environment, applicationId, hostId) };
76
+ }
77
+ catch (error) {
78
+ if (!(error instanceof PlatformApiError) || error.status !== 404)
79
+ throw error;
80
+ }
81
+ }
82
+ throw new PlatformApiError(`flow_get: bu kimlikte sayfa/layout/komponent yok (${hostId}, uygulama ${applicationId}). hostId akışın barındırıcısının id'sidir, akışın kendi id'si değil — page_list / layout_list / component_list ile doğrulayın ya da host'u açıkça verin.`, 404);
83
+ }
67
84
  /**
68
85
  * TB-126 / S2-36 — satır aksiyonu taşıyabilen komponentler. Bu tiplerin `actions` prop'unu çoğu
69
86
  * zaman YÜKLEME AKIŞI çalışma zamanında yazar (aksiyon role göre eklenip çıkarılıyorsa başka yolu
@@ -529,16 +546,26 @@ export function registerFlowTools(server, platform) {
529
546
  server.registerTool('flow_get', {
530
547
  title: 'Akış getir',
531
548
  description: "Tek akışı adımlarıyla getirir; yeni akış yazarken en iyi şablon budur. bindings bu akışa bağlı olaylardır ([{ target: elementId | 'page', event }]; boşsa akış sahipsizdir). Hedefi silinmiş (element'siz) setter iteratee'si varsa yanıttaki flowWarnings söyler — o adım koşar ama hiçbir şeye yazmaz.",
532
- inputSchema: { project: projectParam, environment: environmentParam, host: hostParam, applicationId: guid, hostId: guid, flowId: flowIdParam },
549
+ inputSchema: {
550
+ project: projectParam,
551
+ environment: environmentParam,
552
+ host: hostParam.optional().describe("Akışın barındırıcısı: page | layout | component. Verilmezse gateway hostId'yi sırayla sayfa → layout → komponent olarak okur (en çok üç okuma, 404 sonrakine geçer) ve yanıtın hostResolvedAs alanı bulduğunu söyler; biliyorsanız verin, tek okuma yapılır"),
553
+ applicationId: guid,
554
+ hostId: guid,
555
+ flowId: flowIdParam
556
+ },
533
557
  annotations: readOnly
534
558
  }, handle(config, async ({ project, environment, host, applicationId, hostId, flowId }) => {
535
- const response = await platform.tenant('GET', project, flowPath(host, applicationId, hostId, flowId), { environment });
536
- const record = await fetchHost(platform, host, project, environment, applicationId, hostId).catch(() => undefined);
559
+ const resolved = host === undefined ? await resolveFlowHost(platform, project, environment, applicationId, hostId) : undefined;
560
+ const owner = resolved?.host ?? host;
561
+ const response = await platform.tenant('GET', project, flowPath(owner, applicationId, hostId, flowId), { environment });
562
+ const record = resolved?.record ?? await fetchHost(platform, owner, project, environment, applicationId, hostId).catch(() => undefined);
537
563
  const warnings = targetlessSetterWarnings(response);
538
564
  if (!response || typeof response !== 'object' || Array.isArray(response))
539
565
  return response;
540
566
  return {
541
567
  ...response,
568
+ ...(resolved === undefined ? {} : { hostResolvedAs: resolved.host }),
542
569
  ...(record === undefined
543
570
  ? { bindingsNote: BINDINGS_UNAVAILABLE }
544
571
  : { bindings: flowBindings(hostNodes(record), record.events).get(flowId.toLowerCase()) ?? [] }),
@@ -114,6 +114,57 @@ export function registerMobileTools(server, platform) {
114
114
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid.describe('Mobil uygulama (slug da verilebilir, gateway GUID\'e çevirir)'), ...pushAudienceFields, pageIndex: pagingParams.pageIndex, pageLength: pagingParams.pageLength.describe('Sayfa boyu; varsayılan 25') },
115
115
  annotations: readOnly
116
116
  }, handle(config, ({ project, environment, applicationId, ...query }) => platform.tenant('GET', project, `/push/devices/${applicationId}`, { environment, query })));
117
+ server.registerTool('mobile_error_list', {
118
+ title: 'Telefonda düşen akış hataları',
119
+ description: "Mobil uygulamanın TELEFONDA düşen akış hatalarını okur (kabuk gönderir, sunucu saklar — defter-saha-bulgulari b-10/f-10). Kayıt biçimi page_screenshot.runtimeErrors ile aynıdır (flowId · step{id,name,type} · code · message · element · property · kind) ve şunları ekler: pageSlug, platform, appVersion, devices (kaç kurulumda), count, lastAt. Aynı akış/adım/mesaj üçlüsü tek satıra katlanır, en yeni önce. Önizlemede görülmeyen cihaz hatasını (ağsız yazma, sayfa kapanınca atlanan adım `page-unmounted`, geri gidilemeyen `back-unavailable`) kullanıcının anlatmasını beklemeden okumak için. Kabuk hatayı ağ gelince gönderir: uçak kipinde düşen hata ağ dönene kadar görünmez; occurredAt telefondaki saattir. Sunucu kurulum başına son 200 kaydı ve 7 günü tutar; mesaj gateway maskesinden geçer. Uygulama ayarı properties.mobile.reportErrors: false gönderimi kapatır. Web önizlemesinin hataları burada YOK — onları page_screenshot.runtimeErrors verir.",
120
+ inputSchema: {
121
+ project: projectParam,
122
+ environment: environmentParam,
123
+ applicationId: guid.describe('Mobil uygulama (slug da verilebilir, gateway GUID\'e çevirir)'),
124
+ pageSlug: z.string().min(1).optional().describe('Yalnız bu sayfada düşenler'),
125
+ flowId: z.string().min(1).optional().describe('Yalnız bu akış'),
126
+ installationId: z.string().min(1).optional().describe('Yalnız bu kurulum (notification_device_list\'teki installationId ile aynı kimlik)'),
127
+ since: z.string().min(1).optional().describe('ISO zaman; yalnız bundan sonra görülenler'),
128
+ limit: z.number().int().min(1).max(100).optional().describe('Katlanmış satır sayısı; varsayılan 20')
129
+ },
130
+ annotations: readOnly
131
+ }, handle(config, async ({ project, environment, applicationId, limit = 20, ...query }) => {
132
+ let response;
133
+ try {
134
+ response = await platform.tenant('GET', project, `/runtime-errors/${applicationId}`, { environment, query: { ...query, limit: 200 } });
135
+ }
136
+ catch (error) {
137
+ if (error instanceof PlatformApiError && error.status === 404) {
138
+ throw new PlatformApiError('mobile_error_list: bu backend istemci akış hatası kaydetmiyor (runtime-errors ucu yok) — backend imajı güncel değil ya da uç bu kurulumda kapalı. Önizleme hatalarını page_screenshot.runtimeErrors verir.', 404);
139
+ }
140
+ throw error;
141
+ }
142
+ const rows = unwrap(response) ?? [];
143
+ // Aynı akış/adım/mesaj tek satır: sayılar toplanır, en yeni kaydın bağlamı (sayfa, sürüm, platform) taşınır.
144
+ const folded = new Map();
145
+ for (const row of rows) {
146
+ const key = `${row.flowId ?? ''}|${row.step?.id ?? ''}|${row.message ?? ''}`;
147
+ const lastAt = row.lastAt ?? row.occurredAt ?? '';
148
+ const existing = folded.get(key);
149
+ if (!existing) {
150
+ folded.set(key, { ...row, lastAt, count: row.count ?? 1, devices: new Set(row.installationId ? [row.installationId] : []) });
151
+ continue;
152
+ }
153
+ existing.count = (existing.count ?? 1) + (row.count ?? 1);
154
+ if (row.installationId)
155
+ existing.devices.add(row.installationId);
156
+ if (lastAt > (existing.lastAt ?? ''))
157
+ Object.assign(existing, { ...row, lastAt, count: existing.count, devices: existing.devices });
158
+ }
159
+ const errors = [...folded.values()]
160
+ .sort((left, right) => (right.lastAt ?? '').localeCompare(left.lastAt ?? ''))
161
+ .map(({ devices, installationId: _installation, ...error }) => ({ ...error, devices: devices.size }));
162
+ return {
163
+ errors: errors.slice(0, limit),
164
+ ...(errors.length > limit ? { truncated: true, total: errors.length } : {}),
165
+ ...(errors.length === 0 ? { note: 'Bu süzgeçle kayıt yok. Telefon hatayı ağ gelince gönderir; uçak kipindeki hata ağ dönene kadar görünmez. Uygulama ayarı properties.mobile.reportErrors: false ise kabuk göndermez.' } : {})
166
+ };
167
+ }));
117
168
  server.registerTool('notification_create', {
118
169
  title: 'Push bildirimi gönder (varsayılan kuru koşu)',
119
170
  description: "Kitleye dile göre içerikle push bildirimi gönderir — `SendPushNotificationCommand: { dryRun, audience, content, defaultLanguage, path?, query?, androidChannel?, badge?, testInstallationId? }`. dryRun VARSAYILAN TRUE: yalnız kitleyi sayar ({ total, byLanguage }), hiçbir şey yazmaz; göndermek için dryRun: false AÇIKÇA verilir. Gerçek gönderim kuyruğa girer ve yanıt id döner; sonucu notification_history { id } ile okuyun (birkaç saniye sonra sent/failed, teslim sayıları ve hatalı cihazlar). Kitle her zaman aktif ve izinli cihazlara daraltılır; testInstallationId verilirse yalnız o kurulum (notification_device_list'teki installationId). İçerik dil → { title, body }; başlığı boş dil düşer ve o cihaz varsayılan dili alır; varsayılan dilde başlık zorunlu (NTF-002, platform 400). Dil anahtarları küçük harfe indirilir. path dokunuşta açılacak sayfanın slug'ı, query onun sorgusu. Ret: production'da dryRun: false istek gitmeden reddedilir (NTF-001).",
@@ -169,6 +220,11 @@ export function registerMobileTools(server, platform) {
169
220
  const plan = buildMobileAppPlan(input);
170
221
  if (dryRun)
171
222
  return { dryRun: true, plan };
223
+ // APP-001 (K5, b-15): sekme ve ana sayfa ayarı yalnız development'a yazılır. Dalda kurulan sayfalara bağlı ayar
224
+ // development'ta olmayan sayfaları gösterir, dal sekmesiz kalırdı.
225
+ if (platform.resolveEnvironment(environment) !== 'development') {
226
+ throw new PlatformApiError(`mobile_app_create [APP-001]: uygulama ayarı (sekmeler, ana sayfa) yalnız development'a yazılır; '${platform.resolveEnvironment(environment)}' ortamında kurulan sayfalara bağlanamaz. Mobil uygulamayı development'ta kurun, production'a deployment_start ile taşıyın. Hiçbir şey yazılmadı.`, 400);
227
+ }
172
228
  const completed = [];
173
229
  const skipped = new Set();
174
230
  const warnings = [];