@appaflytech/zeyrek-ai-mcp 0.0.40 → 0.0.41

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.
@@ -179,12 +179,12 @@ istekle gider.
179
179
  Gateway alan tipini şemadan bilemediği için uyarı metinseldir: `.value` ile biten en az üç parçalı
180
180
  yol (`urlInput.value` gibi bileşen değeri ve `fileInput1.value.value` uyarı almaz).
181
181
 
182
- Workflow'un dosya yükleme adımı yalnız `{ id }` döndürür (URL yok); görsel adresi kaydı okuyan uçtan
183
- gelir. `page_screenshot` ile listede görseli doğrulayın.
182
+ Workflow'un `file-upload` adımı da aynı nesneyi döndürür, adres `<adım>.data.absolute_path`
183
+ (`platform_guide('workflow')`). `page_screenshot` ile listede görseli doğrulayın.
184
184
 
185
185
  ## Liste ve boş durumu
186
186
 
187
- Mobil `repeater`'ın çocukları TAM İKİ `view`'dır (web listesi tek şablonludur, bu şekil YALNIZ mobildir) — satır şablonu ve boş görünüm (ADR
187
+ Mobil `repeater`'ın çocukları TAM İKİ `view`'dır (web'de tek şablon) — satır şablonu ve boş görünüm (ADR
188
188
  docs/adr/0005; başka şekil 422 `fixed-views`). Satır yokken boş görünüm bir kez, tam genişlikte
189
189
  çizilir; boş ekranı oraya tasarla. `emptyTitle`/`emptyDescription`/`emptyIcon` prop'ları ve kardeş
190
190
  `emptystate` yolu 2026-09-30'da kaldırıldı. Mobil `view` çocuklarını sütunda dizer. Tam gövde:
@@ -204,15 +204,15 @@ bölümleri bileşen render anında çıkarır.
204
204
  3. **Çocuklar konumsaldır:** çocuk 0 bölüm başlığı, çocuk 1 öğe şablonu; fazlası çizilmez.
205
205
 
206
206
  ```json
207
- { "elementId": "tasks", "name": "tasks", "type": "grouped-list",
207
+ { "elementId": "tasks", "name": "tasks", "type": "grouped-list", "blocks": [],
208
208
  "props": { "rows": { "type": "code", "ref": "{{ q_tasks.data }}" },
209
- "groupBy": { "type": "static", "data": "dueDate" },
210
- "itemPressable": { "type": "static", "data": true } },
209
+ "groupBy": { "type": "static", "data": "dueDate" },
210
+ "itemPressable": { "type": "static", "data": true } },
211
211
  "children": [
212
- { "elementId": "day_title", "name": "day_title", "type": "text",
212
+ { "elementId": "day_title", "name": "day_title", "type": "text", "blocks": [],
213
213
  "props": { "content": { "type": "code", "ref": "{{ Section.title }} · {{ Section.count }}" } } },
214
- { "elementId": "task_row", "name": "task_row", "type": "container", "props": {},
215
- "children": [ { "elementId": "task_title", "name": "task_title", "type": "text",
214
+ { "elementId": "task_row", "name": "task_row", "type": "container", "props": {}, "blocks": [],
215
+ "children": [ { "elementId": "task_title", "name": "task_title", "type": "text", "blocks": [],
216
216
  "props": { "content": { "type": "code", "ref": "{{ item.title }}" } } } ] } ] }
217
217
  ```
218
218
 
@@ -241,10 +241,10 @@ düğümü sayfa kök kabının SONUNA koyun (akıştaki yeri boş kalır).
241
241
  sayılır), `respectSafeArea` (varsayılan açık). Alt eylem çubuğu:
242
242
 
243
243
  ```json
244
- { "elementId": "action_bar", "name": "action_bar", "type": "floating-container",
244
+ { "elementId": "action_bar", "name": "action_bar", "type": "floating-container", "blocks": [],
245
245
  "props": { "anchor": { "type": "static", "data": "bottom-center" },
246
- "fullWidth": { "type": "static", "data": true } },
247
- "children": [ { "elementId": "bar", "name": "bar", "type": "container",
246
+ "fullWidth": { "type": "static", "data": true } },
247
+ "children": [ { "elementId": "bar", "name": "bar", "type": "container", "blocks": [],
248
248
  "props": { "direction": { "type": "static", "data": "row" } }, "children": [] } ] }
249
249
  ```
250
250
 
@@ -411,8 +411,8 @@ Motorun kendi adımları (`setter`, `condition`, `microservice`, `code`, `functi
411
411
  |---|---|
412
412
  | `redirect` | yığına iter (göreli yol) ya da dışarıda açar (tam adres) |
413
413
  | `navigate` | eski mobil şemaların adı — `redirect`'in takma adıdır; gateway `redirect`'e çevirir |
414
- | `alert` | tipli uyarı (`success`/`error`/`info`/`warning`) toast'tur ve beklemez; düz uyarı onay bekler. 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 }`) |
415
- | `confirm` | `yes` / `no` dalları |
414
+ | `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 }`) |
415
+ | `confirm` | `yes` / `no` dalları; `message`/`title`/`confirmText`/`cancelText` metin şablonudur (`{{ }}`, çeviri `t('anahtar')`) |
416
416
  | `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) |
417
417
  | `storage` | `args.name` `state.page.<key>` / `state.app.<key>` ise o state'i yazar (§Kalıcı state), değilse cihaz deposuna |
418
418
  | `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 |
@@ -421,10 +421,11 @@ Motorun kendi adımları (`setter`, `condition`, `microservice`, `code`, `functi
421
421
  | `location` | cihaz konumu (`expo-location`; ilk kullanımda izin sorulur; derleme konumu taşımıyorsa sorulmaz, `error` dalı `status: 'unavailable'`). `args.value` doğruluk, düz metin: `low` · `balanced` · `high` (boş → `balanced`); `args.isRaw: true` → adres çözülmez. Sonuç `Steps.<adım>.data = { latitude, longitude, accuracy, address }`, `address = { country, region, subregion, city, district, street, postalCode }` ya da `null` (Türkiye'de il `region`, ilçe `subregion`, yoksa `city`). İzin reddi `Steps.<adım>.error = { status: 'denied', message }`, okunamayan konum `unavailable`: adımın `error` dalı bağlıysa o koşar (`always` koşmaz), yoksa akışın hata akışı, o da yoksa `always` (`data: null`). Web'de tarayıcı konumu, adres hep `null` |
422
422
 
423
423
  | `notification-permission` | bildirim izni ister (sorulmuşsa yeniden sormaz, kalıcı red yeniden sorulmaz). Sonuç `Steps.<adım>.data.status`: `granted` · `denied` · `undetermined`; red hata değildir, hep `always` |
424
- | `notification-schedule` | yerel bildirim planlar (`expo-notifications`; uzak push değil). `args.name` kimlik (düz metin; aynı kimlik eskisinin yerini alır), `args.title` başlık (düz metin), `args.message` gövde (**ifade**), `args.value` zaman (**ifade**): sonucu `'HH:mm'` → her gün, `{ weekday: 1–7, time: 'HH:mm' }` → her hafta (1 = Pazartesi), ISO tarih-saat → bir kez. `args.path` dokununca açılacak sayfanın slug'ı (+ `args.query` ifade), boşsa uygulama açılır. İzin yoksa `error.status: 'denied'`, geçmiş ya da okunamayan zaman `invalid`: `error` dalı → hata akışı → `always`. Web'de desteklenmez (`unsupported`) — sayfa kapalıyken tarayıcı gösteremez |
425
- | `notification-cancel` | `args.name` kimlikli planlı bildirimi siler, `'*'` hepsini; hep `always` |
424
+ | `notification-schedule` | yerel bildirim planlar (`expo-notifications`; uzak push değil). `args.name` kimlik ve `args.title` başlık **metin şablonudur** — yalnız `{{ }}` içi çözülür (`'ilac-{{ item.id }}-0800'` kayıt başına ayrı kimlik, `"{{ t('ilac.vakti') }}"` çeviri; `{{ }}` dışındaki ifade çözülmez, FLOW-022); aynı kimlik eskisinin yerini alır. `args.message` gövde (**ifade**), `args.value` zaman (**ifade**): sonucu `'HH:mm'` → her gün, `{ weekday: 1–7, time: 'HH:mm' }` → her hafta (1 = Pazartesi), ISO tarih-saat → bir kez. `args.path` dokununca açılacak sayfanın slug'ı, düz metin — şablon çözmez (+ `args.query` ifade), boşsa uygulama açılır. İzin yoksa `error.status: 'denied'`, geçmiş ya da okunamayan zaman `invalid`: `error` dalı → hata akışı → `always`. Web'de desteklenmez (`unsupported`) — sayfa kapalıyken tarayıcı gösteremez |
425
+ | `notification-cancel` | `args.name` kimlikli planlı bildirimi siler (planlamadaki gibi metin şablonu: `'ilac-{{ item.id }}-0800'`), `'*'` hepsini; hep `always` |
426
426
  | `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) |
427
427
  | `open-settings` | uygulamanın sistem ayar sayfasını açar; `Steps.<adım>.data.opened`. Argüman almaz |
428
+ | `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 |
428
429
  | `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 |
429
430
 
430
431
  Bildirim adımları paletten yalnız mobil uygulamada sunulur. Tarif — S04 "her gün hatırlat": önce
@@ -455,7 +456,11 @@ ayarda tanımlı olduğu sayfa/akış yazımında denetlenemez — `mobile_store
455
456
  düğmenin `click`'i → `location` (`always` → setter `il.value = Steps.konum.data.address?.region`, `error` →
456
457
  setter hata metni). Uzak push (sunucudan gelen bildirim) ayrı bölümde: §Uzak push.
457
458
 
458
- Sayfa ve layout `mount` akışları ilk çizimden sonra, `destroy` akışları sayfa kalkarken koşar.
459
+ Sayfa ve layout `mount` akışları ilk çizimden sonra, `destroy` akışları sayfa kalkarken koşar. Yığındaki
460
+ ve sekmedeki ekran bağlı kalır: geri dönüşte `mount` KOŞMAZ. Listeyi yeniden okumak için sayfanın
461
+ **`focus`** olayı vardır (layout'ta yok): ilk görünüşte `mount`'tan sonra ve her geri dönüşte / sekme
462
+ değişiminde koşar. Bağ: `page_update` `events: [{ name: 'focus', value: <flow id> }]`. Form doldurma
463
+ `mount`'ta, liste okuması `focus`'ta — aynı okuma ikisine bağlanırsa ilk açılışta çift istek gider.
459
464
  `Functions.<ad>()` uygulama ve sayfa fonksiyonlarını görür (`platform_guide('functions')`).
460
465
 
461
466
 
@@ -488,7 +493,8 @@ Sunucudan gönderilen bildirim yerel `notification-schedule`'dan ayrıdır. Kana
488
493
  ## Kalıcı state
489
494
 
490
495
  State varsayılan olarak **bellektedir**: sayfa her açıldığında `variable_create`'te yazılan `value`'dan
491
- başlar, akışın yazdığı değer ekran kapanınca kaybolur. Uygulama kapanıp açıldığında kaldığı yerden
496
+ başlar, akışın yazdığı değer ekran kapanınca kaybolur — `state.app` dahil: sayfalar arası
497
+ taşımak da `storage` ister (38 S-5). Uygulama kapanıp açıldığında kaldığı yerden
492
498
  sürmesi gereken değer için değişkene saklama türü verin:
493
499
 
494
500
  | `storage` | Depo | Ne için |
@@ -526,7 +532,10 @@ variable_create({ applicationId, pageId, variables: [
526
532
  780x1688 px) çeker; `'tablet'` 820x1180 @2 ya da `{ width, height, scale? }`. `viewport` verilirse
527
533
  cihaz yok sayılır (`deviceNote`). Yanıt `applicationType: 'mobile'` ve `platformNote` taşır: kare
528
534
  react-native-web çizimidir, cihazdakiyle yapısal olarak (düğüm, sıra, ölçü, renk) aynıdır.
529
- - Eylemler aynı sözlük: `click` basılabilir öğeyi (`role="button"`) bulur; `submit` runtime
535
+ - Eylemler aynı sözlük: `click` önce düğümün kendisine (basılabilirse: `role="button"`/`"switch"`/
536
+ `"combobox"`), sonra `<elementId>__press` yüzeyine, en son içteki ilk düğmeye basar — anahtar ve
537
+ onay kutusu kök kimlikle çevrilir, çoklu seçimi `<elementId>__trigger` açar (çipin "kaldır"ına
538
+ basmaz); `set` alana odaklanır, yazar ve odaktan çıkar (`changed` koşar); `submit` runtime
530
539
  köprüsüyle gider (`submitVia: 'bridge'`); sekme öğesi `__tabs__item-<sayfa slug'ı>`, çubuk `__tabs`
531
540
  özet satırıdır. `select` açılır listeyi açar ve seçeneği değerle ya da görünen metinle seçer
532
541
  (liste katmanda açılır, `optionsSeen` kaç seçenek görüldüğünü söyler); `tab` bir `tabs`
@@ -282,7 +282,8 @@ dalı bulamaz ve akışı **sessizce durdurur** (`flow-engine.ts:79`); Studio da
282
282
  İstisnalar: `condition` → dal adı rule id'sidir (ve o id bir **GUID**'dir — aşağıdaki tuzak 6),
283
283
  `confirm` → `yes|no|always`, `microservice` →
284
284
  `always` (eski akışlarda `success` de çalışır) **+ `error`** — bağlarsan hata anında `always`
285
- yerine ondan sürer (2026-09-26, ADR 0004; ayrıntı aşağıdaki §"4xx de hatadır").
285
+ yerine ondan sürer (2026-09-26, ADR 0004; ayrıntı aşağıdaki §"4xx de hatadır"), `validation` →
286
+ `success` (hepsi geçti) | `error` (biri düştü), ardından her durumda `always` (kuralları aşağıda).
286
287
 
287
288
  **2. `iteratees[].type` yalnız `change` | `fill`.** `setter.ts` başka değerde hiçbir dala
288
289
  girmez — adım çalışır, hiçbir şey yapmaz. `change` tek prop'a değer yazar, `fill` diziyi
@@ -445,13 +446,15 @@ göstermek error flow'un işidir.
445
446
  > | `Steps.<adımAdı>.error.body` | Ucun hata gövdesi — eski `Steps.<adımAdı>.data`'nın taşındığı yer | `microservice.ts` `.catch` |
446
447
  > | `Steps.<adımAdı>.data` | **HER ZAMAN `null`**'dır — başarı gövdesi değil, hiçbir gövde değil (eskiden hata gövdesiydi) | `microservice.ts` `.catch` |
447
448
  >
448
- > **`alert` adımının `message`'ı DÜZ METİNDİR — ifade yazarsan ekranda o ifade görünür.**
449
- > `alert.ts` `task.argument.message`'ı doğrudan `toast.error()`'a geçirir ve `args` hiçbir yerde
450
- > değerlendirilmez (`Runtime.svelte` `argument: step.args`): `{ "type": "alert", "args": {
451
- > "message": "Steps.error.message", "alertType": "error" } }` kullanıcıya harfi harfine
452
- > `Steps.error.message` gösterir. Sabit bir uyarı yetiyorsa `alert` en ucuz adımdır; **ucun ne
453
- > dediğini göstereceksen bir `setter` (ya da `code`) adımı zorunludur** — `value.type: 'code'`
454
- > ifadeyi orada gerçekten çalıştırır:
449
+ > **`alert` adımının `message`'ı ve `description`'ı METİN ŞABLONUDUR — yalnız `{{ }}` içi çözülür.**
450
+ > Bileşen prop bağlamasıyla aynı çözücü (`@ng-apps/flow` `resolveStepText` → `runPropertyCode`; web ve
451
+ > cihaz aynı): `{ "type": "alert", "args": { "message": "{{ Steps.error?.message ?? 'İşlem başarısız' }}",
452
+ > "alertType": "error" } }` ucun mesajını gösterir, `"{{ t('kayit.tamam') }}"` etkin dildeki çeviriyi.
453
+ > `{{ }}` DIŞINDA yazılan ifade çözülmez: `"message": "Steps.error.message"` kullanıcıya harfi harfine
454
+ > `Steps.error.message` gösterir (FLOW-022 uyarır). `{{ }}` içermeyen metin olduğu gibi kalır. 2026-10-08
455
+ > öncesi runtime'da (yeniden yayımlanmamış kurulum, eski mobil derleme) bu alanlar düz metindi; orada
456
+ > ucun mesajı bir `setter` (ya da `code`) adımıyla bir bileşene yazılır — mesajı ekranda kalıcı
457
+ > göstermek için bugün de bu yol geçerlidir:
455
458
  >
456
459
  > ```jsonc
457
460
  > // errorSteps: start → bu setter. Metni bir notification/text komponentine yazar.
@@ -463,13 +466,14 @@ göstermek error flow'un işidir.
463
466
  > "branches": [{ "id": "<GUID>", "name": "always", "nextStepId": null }] }
464
467
  > ```
465
468
 
466
- > **`confirm` adımının diyalog çerçevesi de yazılabilir.** `args` dört düz metin alanı taşır:
469
+ > **`confirm` adımının diyalog çerçevesi de yazılabilir.** `args` dört metin alanı taşır:
467
470
  > `message` (gövde), `title` (başlık), `confirmText` (onay düğmesi, `yes` dalı), `cancelText`
468
471
  > (vazgeç düğmesi, `no` dalı). Son üçü isteğe bağlıdır; boş bırakılan alan dile göre sabit metne
469
472
  > düşer ("Emin misiniz?" / "Evet" / "Hayır" — `dialog.functions.ts` `resolveDialogFrame`).
470
- > Dördü de `alert`'in `message`'ı gibi DÜZ METİNDİR (FLOW-022 hepsini denetler):
471
- > `{ "type": "confirm", "args": { "message": "Bu risk ve 4 aksiyonu silinecek.", "title": "Riski sil",
472
- > "confirmText": "Sil", "cancelText": "Vazgeç" } }`.
473
+ > Dördü de `alert`'in `message`'ı gibi METİN ŞABLONUDUR (FLOW-022 hepsini denetler); çok dilli
474
+ > uygulamada çeviri anahtarıyla yazılır:
475
+ > `{ "type": "confirm", "args": { "message": "{{ t('risk.sil.onay') }}", "title": "{{ Row.name }} silinsin mi?",
476
+ > "confirmText": "{{ t('sil') }}", "cancelText": "Vazgeç" } }`.
473
477
 
474
478
  > **Hangi yolu seçeceksin — adımın kendi `error` dalı mı, `errorSteps` mi, ana akışta
475
479
  > `condition` mı?** Bir hatada sırayla denenir (yukarıdaki 4xx bölümü) — üçü birden TANIMLI
@@ -528,6 +532,30 @@ Tarif: `platform_guide('middleware')`.
528
532
  - Adım hedef akışı **bekler** (`await engine.run`), sonra `always` dalından devam eder.
529
533
  - Gateway boş/yarım `args`'ı **FLOW-014** ile yakalar.
530
534
 
535
+ **`validation` adımının kuralları `args.validations` dizisindedir** (kök ADR 0014). Her kural
536
+ `{ id?, component, property, type, errorMessage, settings }`: `component` sayfadaki bileşenin
537
+ **elementId**'si (boş → bileşensiz kural; sayfada olmayan ad 404), `property` okunacak prop
538
+ (çoğunlukla `value`), `type` ∈ `required · email · regex · min-length · max-length · min-value ·
539
+ max-value`, `errorMessage` düz metin ya da `t:` anahtarı (düşen alanın `invalidText`'ine yazılır),
540
+ `settings` tek parametredir — runtime **ilk değeri** okur (`{ "value": 8 }`, `{ "pattern": "^\\d+$" }`;
541
+ parametresiz tipte `{}`). Kurallar dizi sırasıyla koşar; kuralsız adım yükte `[]` döner ve her
542
+ çalıştırmada `success`'e gider.
543
+
544
+ ```jsonc
545
+ { "stepId": "kontrol", "name": "kontrol", "type": "validation", "kind": "normal",
546
+ "args": { "validations": [
547
+ { "component": "email", "property": "value", "type": "email", "errorMessage": "t:validation.email", "settings": {} },
548
+ { "component": "sifre", "property": "value", "type": "min-length", "errorMessage": "En az 8 karakter", "settings": { "value": 8 } } ] },
549
+ "branches": [ { "id": "<GUID>", "name": "success", "nextStepId": "kaydet" },
550
+ { "id": "<GUID>", "name": "error", "nextStepId": null } ] }
551
+ ```
552
+
553
+ - `always` `success`/`error`'dan **sonra** da koşar — kaydetmeyi `always`'e bağlarsan düşen
554
+ formda da kaydeder.
555
+ - Bu şekil komponentin kendi `validations`'ından (`set_validations { type, message, value }`)
556
+ farklıdır; `message`/`value` burada sessizce düşer. Tanınmayan `type` runtime'da **geçer** sayılır
557
+ (platform kümeyi doğrulamaz). Gateway ikisini de **FLOW-041** ile yakalar.
558
+
531
559
  > **`code` adımının gövdesi `args.value` alanındadır — düz JS metni olarak.** Runtime onu
532
560
  > `(async () => { <value> })()` içinde koşturur (`code.ts:18`), yani `await` serbesttir ve dönüş
533
561
  > değeri akışa taşınmaz; etkiyi `Page`/`state`/komponent yazarak üretirsin. **Alan boşsa adım
@@ -618,7 +646,7 @@ değer tipleri: `static · page · property · storage · variable · code · it
618
646
  > **`platform_guide('functions')`**.
619
647
  >
620
648
  > **`share` adımı ("Share")** metin ve bağlantı paylaşır: `args: { title, message, path }`. `title`
621
- > DÜZ METİNDİR (ifade yazılırsa FLOW-022 uyarır); `message` ve `path` `storage.value` gibi
649
+ > DÜZ METİNDİR — `{{ }}` şablonu da çözülmez (ifade ya da `{{ }}` yazılırsa FLOW-022 uyarır); `message` ve `path` `storage.value` gibi
622
650
  > İFADEDİR — düz metin tırnak ister (`"'Şuna bak'"`, `"'https://…/urun/' + Page.queryParams.id"`).
623
651
  > Bağlantının adı `url` DEĞİL `path`'tir: backend `Title`/`Message`/`Path` alanlarında saklar ve
624
652
  > tanımadığı anahtarı 200 ile sessizce düşürür (FLOW-035 `url`/`link`/`text`'i ve tırnaksız adresi
@@ -937,13 +965,16 @@ kapsayıcının kardeşi olarak yaşar (qm `[SY] Profil bilgilerim`: `page_conta
937
965
  > Bu tiplerde `size.h` gerçek yüksekliktir — göz kararı değil, içeriğe göre hesapla
938
966
  > (bir satır ≈ 10px; Kural 1f).
939
967
 
940
- ## Kural 1d-2 — Sayfanın ve layout'un KENDİ olayları: `mount` · `destroy`
968
+ ## Kural 1d-2 — Sayfanın ve layout'un KENDİ olayları: `mount` · `destroy` (mobil sayfada + `focus`)
941
969
 
942
970
  Komponent olaylarından AYRI olarak sayfa ve layout kayıtları kendi olay listesini taşır
943
971
  (`events: [{ name, value: <flow id> }]`) ve geçerli adlar SABİTTİR: **`mount`**
944
972
  (yüklendiğinde) ve **`destroy`** (sayfadan/layouttan ayrılırken — runtime destroy akışlarını
945
- navigasyondan ÖNCE koşar). Başka ad platformca kabul edilir ama bağ sessizce ölü kalır —
946
- gateway **PAGE-011 / LAY-004** ile istek gitmeden reddeder.
973
+ navigasyondan ÖNCE koşar). **Mobil uygulamanın SAYFASI** bir ad daha alır: **`focus`** — ekran
974
+ her göründüğünde koşar (ilk açılışta `mount`'tan sonra, sonra yığından geri dönüşte ve sekme
975
+ değişiminde; `platform_guide('mobile')`). Başka ad platformca kabul edilir ama bağ sessizce ölü
976
+ kalır — gateway **PAGE-011 / LAY-004** ile istek gitmeden reddeder; `focus` layout'ta ve web
977
+ sayfasında da reddedilir (web sayfayı her gezinmede baştan kurar, `mount` yeter).
947
978
 
948
979
  - Bağlama yolu `set_event` DEĞİLDİR (o komponent hedefler): sayfada `page_create` /
949
980
  `page_update`'in `events` parametresi, layout'ta `layout_create/update`'in `events`'i.
@@ -55,7 +55,8 @@ diye bir alan **hiç görünmez**; blok tam olarak bunu görünür kılmak için
55
55
  | Tool | Ne yapar |
56
56
  |---|---|
57
57
  | `translation_list` | Application'ın tüm `(language, key, value)` kayıtlarını düz liste döner |
58
- | `translation_update` | `keys` (ekle/güncelle, `componentId` opsiyonel) ve/veya `deletedKeys` (sil) — ikisi de aynı çağrıda verilebilir, en az biri dolu olmalı |
58
+ | `translation_update` | `keys` (ekle/güncelle, `componentId` opsiyonel) ve/veya `deletedKeys` (sil) — ikisi de aynı çağrıda verilebilir, en az biri dolu olmalı. `file` kipi: doldurulmuş Excel'i içe aktarır (aşağıda) |
59
+ | `translation_export` | Çevirmene verilecek Excel dosyasını yerel yola yazar (aşağıda) |
59
60
  | `project_update` | `languages`/`defaultLanguage` — proje dillerini kurar/değiştirir |
60
61
 
61
62
  ```jsonc
@@ -194,6 +195,45 @@ Runtime `t:` ile başlayan bir mesajı anahtar sayar ve normal zincirden çözer
194
195
  olduğu gibi gösterilir. Çözüm çalışma anında olduğu için şema varsayılanlarını değiştirmek
195
196
  gerekmez — `schema.ts`'e dokunulmaz, katalog yeniden yüklenmez.
196
197
 
198
+ ## Dosyayla toplu çeviri (Excel)
199
+
200
+ İnsan bir çevirmene yeni bir dil yaptıracaksa: sözlük Excel'e iner, çevirmen hedef sütunu doldurur,
201
+ dosya geri yüklenir. **Çeviriyi sen yapacaksan dosyaya gerek yok** — `translation_list` ile oku,
202
+ `translation_update { keys }` ile yaz.
203
+
204
+ ```jsonc
205
+ // 1. dışa aktar — dosya YALNIZ outPath'e yazılır, Sürücü'ye yüklenmez
206
+ { "applicationId": "<app id>", "targetLanguage": "es", "outPath": "C:/ceviri/crm-es.xlsx" } // translation_export
207
+ // → { keys: 120, targetMissing: 120, sourceLanguage: "en", note: "… TR-004 …" }
208
+
209
+ // 2. çevirmen C sütununu doldurur; 3. önce kuru koşu
210
+ { "applicationId": "<app id>", "file": "C:/ceviri/crm-es.xlsx", "targetLanguage": "es", "dryRun": true } // translation_update
211
+ // → { created: 118, skippedEmpty: 2, unknownKeyCount: 0, targetLanguageInProject: false, note: "… ekleyecek …" }
212
+
213
+ // 4. gerçek içe aktarma
214
+ { "applicationId": "<app id>", "file": "C:/ceviri/crm-es.xlsx", "targetLanguage": "es" } // translation_update
215
+ // → { created: 118, languageAdded: true, … }
216
+ ```
217
+
218
+ Dosya: tek sayfa, `A` Key · `B` kaynak dil (yalnız referans, okunmaz) · `C` hedef dil; başlıklar
219
+ `Key` · `Default Language (EN)` · `Target Language (ES)`. İçe aktarma **başlık satırını okumaz**:
220
+ değerler `translation_update`'in zorunlu `targetLanguage`'ındaki dile yazılır, `C1`'deki `(ES)` yalnız
221
+ çevirmen içindir ve dili belirlemez. Gizli `_zeyrek`
222
+ sayfası dosyanın hangi application'dan çıktığını söyler; başka bir application'ın dosyası yine
223
+ yazılır ama yanıt `applicationMismatch: true` ve not taşır.
224
+
225
+ - **Dolu hücre mevcut değeri ezer**, boş/boşluk hücre atlanır, hiçbir şey silinmez.
226
+ - **Application'da olmayan key açılmaz** — `unknownKeys`'e düşer. Yeni key `keys` kipiyle açılır.
227
+ - Key eşleşmesi önce birebir, yoksa büyük/küçük harf duyarsız tek aday; aynı key dosyada iki satırda
228
+ ise 422 ve hiçbir şey yazılmaz.
229
+ - **Hedef dil projede yoksa içe aktarma onu projeye ekler — bütün ortamlara** (TR-004). Önce
230
+ `project_update` gerekmez; ama dil production'a da gider ve orada çeviriler deployment'la gelene
231
+ kadar runtime varsayılan dile düşer.
232
+ - Dosya platformda **saklanmaz**: ne dışa aktarma ne içe aktarma Sürücü'ye bir şey bırakır.
233
+ - Studio'nun çeviri panelindeki JSON içe aktarma bundan farklıdır: o yalnız boşları doldurur ve
234
+ projede olmayan dili atlar.
235
+ - Sınırlar: `.xlsx` (`.xls`, csv, json reddedilir), 10 MB, 10.000 satır.
236
+
197
237
  ## Bilinen sınırlar
198
238
 
199
239
  - **Flow adımlarında Translation kaynağı YOK.** `value.type` sözlüğünde (`platform_guide('page')`
@@ -286,7 +286,10 @@ Global-error dalında `error` değişkeni tanımlıdır:
286
286
  ```
287
287
 
288
288
  Fail dalında aynı zincir `<adım adı>.error.cause`'tadır (`<adım adı>.error` = `{ code, message, detail,
289
- type, cause }`).
289
+ type, cause }`). Serbest `error` değişkenini YALNIZ global-error dalı kurar
290
+ (`ActivityContext.EnterGlobalErrorHandling`): bağlı `fail` dalının hedefi `error.message` okursa
291
+ `undefined` alır — orada `<adım adı>.error.message` / `.code` okuyun (WF-035 uyarır; hedef aynı zamanda
292
+ global-error dalının hedefi olsa da).
290
293
 
291
294
  `error.step` hangi adımın patladığını söyler. `input`/`data` alanları ifade değerlerini ve adım
292
295
  çıktısını taşır — gövdeye yalnız ihtiyaç duyduğun alt kümeyi yaz (gateway'in varsayılanı
@@ -575,6 +578,33 @@ Tam örnek, ölçülen çıktı şekli ve `code` çocuğu tuzağının çıkış
575
578
  > `The JSON value could not be converted to System.Guid` ile reddetti. Geçerli önek harfleri
576
579
  > yalnız `a`–`f`'tir.
577
580
 
581
+ ## `file-upload` adımı: çıktı, dosya alanının tablo okuma biçimidir (K10, 2026-10-08)
582
+
583
+ Adım `rule.fileString`'i (base64, JS ifadesi) `rule.folderId` klasörüne `rule.fileName` (JS ifadesi;
584
+ boşsa GUID) adıyla yazar, dosyayı geri okur ve **tablo okumasının FILE alanında döndürdüğü nesneyi**
585
+ üretir (`FileUploadStep.OutputSchema`; iki yol aynı `FilePathMapper`'ı kullanır):
586
+
587
+ ```json
588
+ {
589
+ "id": "0199c3a2-7b1e-7c4d-9a10-3f2b8c6d5e41",
590
+ "path": "uploads/photo.png",
591
+ "absolute_path": "https://acme.example.com/dev/uploads/photo.png",
592
+ "type": "ref",
593
+ "value": "0199c3a2-7b1e-7c4d-9a10-3f2b8c6d5e41",
594
+ "fileName": "photo.png"
595
+ }
596
+ ```
597
+
598
+ - Sonraki adımlar **adım adı + `.data`** ile okur: adres `yukle.data.absolute_path`, kimlik
599
+ `yukle.data.id` (`value` aynı id'dir). `absolute_path` = uygulamanın dosya sunum ucu (org slug +
600
+ ortamın depolama öneki) + `/` + `path`; `path` boşsa `""`. Görsel için ikinci okuma gerekmez.
601
+ - **Aynı içerik klasörde varsa** yeni dosya yazılmaz: `id`, adres ve `fileName` var olan dosyanındır.
602
+ - **Mobil görsel:** `response` adımı nesneyi döndürür (`bodyExpression: "({ foto: yukle.data })"`),
603
+ sayfa akışı `Steps.<sayfa adımı>.data.foto`'yu `image.src`'ye verir — nesnenin kendisi ya da
604
+ `.absolute_path`; `.value` dosya kimliğidir, görsel çizilmez (`platform_guide('mobile')` §Dosya ve fotoğraf).
605
+ - 2026-10-08'den eski backend imajı yalnız `{ id }` döndürür: `yukle.data.absolute_path` `undefined`
606
+ çıkıyorsa imaj bayattır. Kaynak: backend `FileUploadStepTests`; canlı koşuyla henüz ölçülmedi.
607
+
578
608
  ## Table step kuralları
579
609
 
580
610
  - `rule` içinde `datasourceId` + `entityId` zorunlu; `selectFields`/`filters` öğelerinde
@@ -1,6 +1,6 @@
1
1
  {
2
- "hash": "63d0b9efc3b8d0c0d8ef9c8b7478b3abf8cb482e4e80276202b3faed73cedddf",
3
- "generatedAt": "2026-10-08T09:32:48.956Z",
2
+ "hash": "1766032527c4215282c73fb003cbe3526babafabc8588b80289dc28fd4e8b393",
3
+ "generatedAt": "2026-10-12T12:46:03.355Z",
4
4
  "types": 337,
5
5
  "files": 167,
6
6
  "note": "Şemaları üreten backend kaynaklarının parmak izi. Elle düzenlemeyin: 'npm run generate-schemas' yazar, 'npm run schema:check' denetler.",
@@ -26,6 +26,40 @@ function preserveApplicationBinding(definition) {
26
26
  if (applicationIds.length > 0)
27
27
  definition.applicationIds = applicationIds;
28
28
  }
29
+ /**
30
+ * 37 S-1: platform PATCH op'larını `EntityDto` OKUMA şekline uygular (`PatchEntityCommandHandler` →
31
+ * `UpdateEntityCommand.FromDto`, `ApplicationIds = Applications.Select(a => a.ApplicationId)`). Bağ
32
+ * `/applications` yolunda `{ applicationId }` öğeleriyle yazılır; `/applicationIds` belgede yoktur
33
+ * (gateway önizlemesi `OPERATION_PATH_UNRESOLVABLE` ile düşer), çıplak guid 500 `JsonException`,
34
+ * `{ id }` boş `applicationId` ile "This record is in use" verir. Ajan yazma adını bilerek geldiği
35
+ * için reddetmek yerine çevrilir ve `gatewayNormalized` ile söylenir (microservice `/roleIds` emsali).
36
+ */
37
+ function normalizeApplicationBindingOps(operations) {
38
+ const notes = [];
39
+ const asBinding = (entry) => {
40
+ if (typeof entry === 'string')
41
+ return { applicationId: entry };
42
+ if (entry && typeof entry === 'object' && !('applicationId' in entry) && typeof entry.id === 'string')
43
+ return { applicationId: entry.id };
44
+ return entry;
45
+ };
46
+ for (const operation of operations) {
47
+ const path = typeof operation.path === 'string' ? operation.path : '';
48
+ if (path === '/applicationIds' || path.startsWith('/applicationIds/')) {
49
+ operation.path = `/applications${path.slice('/applicationIds'.length)}`;
50
+ notes.push(`patch yolu '${path}' → '${operation.path}': yamalanan belge entity_get'in OKUMA şeklidir (EntityDto) ve uygulama bağı orada 'applications' [{ applicationId }] alanıdır; 'applicationIds' yalnız create/update GÖVDESİNDE vardır.`);
51
+ }
52
+ const target = operation.path;
53
+ if (target !== '/applications' && !target.startsWith('/applications/'))
54
+ continue;
55
+ const before = JSON.stringify(operation.value);
56
+ operation.value = target === '/applications' && Array.isArray(operation.value) ? operation.value.map(asBinding) : target.split('/').length === 3 ? asBinding(operation.value) : operation.value;
57
+ if (JSON.stringify(operation.value) !== before) {
58
+ notes.push(`'${target}' değeri { applicationId } öğesine çevrildi — çalışan şekil: { op: 'replace', path: '/applications', value: [{ applicationId: '<application id>' }] }.`);
59
+ }
60
+ }
61
+ return notes;
62
+ }
29
63
  /**
30
64
  * `UpdateEntityCommandHandler` gövdedeki `Indexes` null ise onu BOŞ LİSTEYE çeviriyor
31
65
  * (`request.Indexes?…ToList() ?? []`) ve `Entity.FindProcessedIndexes` boş listede entity'nin
@@ -178,7 +212,7 @@ export function registerEntityTools(server, platform) {
178
212
  }));
179
213
  server.registerTool('entity_patch', {
180
214
  title: 'Entity\'ye patch uygula',
181
- description: "YENİ alanı korumayla ya da türetilmiş alan olarak eklemek için de kullanılır (PROT-003/PROT-004); eklenen ya da kuralı değişen türetilmiş alanı dolu satırlarda derived-fill işi doldurur (tablo kilitlenmez, bitiş import_history { entityId }). VAR OLAN alanın korumasını değiştirmez — platform 400 ProtectionChangeNotAllowed döner, gateway istek göndermeden durdurur ve yol entity_protection_update'tir. Tam gövdeyi bağlama almadan hedefli değişiklik: gateway mevcut tanımı okur, yolları çözer ve op'ları platformun PATCH /entities/{id} ucuna `{ baseVersion, ops }` olarak geçirir — tam gövde PUT'u ATILMAZ, yalnız fark yazılır. Yanıt outline özetidir. YOLLARDA SEÇİCİ KULLANIN, indeks değil: '/fields[slug=email]/nullable' — indeksle yazmak alan sırası değişince SESSİZCE yanlış alanı yazar. Gateway seçiciyi indekse çevirir ve hangi kayda indiğini yanıttaki patchTargets ile söyler. `baseVersion` okunan kaydın `version` alanından gelir; kayıt bu arada değiştiyse platform 409 döner ve HİÇBİR op uygulanmaz. 'applications' → 'applicationIds' çevirisini artık platform yapar. `fields[].isLocalized: true` alanı ana tablodan çıkarıp companion `<slug>_localization` entity'sine taşır (yalnız düz METİN alanı; anahtar/unique/ilişki olamaz) — platform_guide('entity') §'Çoklu dilli alan'.",
215
+ description: "YENİ alanı korumayla ya da türetilmiş alan olarak eklemek için de kullanılır (PROT-003/PROT-004); eklenen ya da kuralı değişen türetilmiş alanı dolu satırlarda derived-fill işi doldurur (tablo kilitlenmez, bitiş import_history { entityId }). VAR OLAN alanın korumasını değiştirmez — platform 400 ProtectionChangeNotAllowed döner, gateway istek göndermeden durdurur ve yol entity_protection_update'tir. Tam gövdeyi bağlama almadan hedefli değişiklik: gateway mevcut tanımı okur, yolları çözer ve op'ları platformun PATCH /entities/{id} ucuna `{ baseVersion, ops }` olarak geçirir — tam gövde PUT'u ATILMAZ, yalnız fark yazılır. Yanıt outline özetidir. YOLLARDA SEÇİCİ KULLANIN, indeks değil: '/fields[slug=email]/nullable' — indeksle yazmak alan sırası değişince SESSİZCE yanlış alanı yazar. Gateway seçiciyi indekse çevirir ve hangi kayda indiğini yanıttaki patchTargets ile söyler. `baseVersion` okunan kaydın `version` alanından gelir; kayıt bu arada değiştiyse platform 409 döner ve HİÇBİR op uygulanmaz. UYGULAMA BAĞI '/applications' yolundadır ve öğeleri { applicationId } nesneleridir: {op:'replace', path:'/applications', value:[{applicationId:'<application id>'}]} — tam listeyi yazar; tek bağ eklemek {op:'add', path:'/applications/-', value:{applicationId:'<application id>'}}. '/applicationIds' yolunu ya da çıplak guid / { id } öğesini gateway bu şekle çevirir ve gatewayNormalized ile söyler; 'applications' → 'applicationIds' gövde çevirisini platform yapar. `fields[].isLocalized: true` alanı ana tablodan çıkarıp companion `<slug>_localization` entity'sine taşır (yalnız düz METİN alanı; anahtar/unique/ilişki olamaz) — platform_guide('entity') §'Çoklu dilli alan'.",
182
216
  inputSchema: { project: projectParam, environment: environmentParam, datasourceId: guid.optional().describe('Verilmezse gateway datasource ağacından bulur'), id: entityRef, operations: patchOperationsParam.optional(), ops: patchOperationsParam.optional().describe("'operations' ile aynı alan (eşanlamlı) — sayfa micro-op tool'larıyla aynı adı kullanabilmeniz için"), policyOverride: policyOverrideParam },
183
217
  annotations: updates
184
218
  }, handle(config, async ({ project, environment, datasourceId, id, operations: givenOperations, ops, policyOverride }) => {
@@ -187,7 +221,10 @@ export function registerEntityTools(server, platform) {
187
221
  const entity = datasourceId && isGuid(id) ? { entityId: id, datasourceId } : await resolver.entity(id);
188
222
  const current = await platform.tenant('GET', project, `/entities/${entity.datasourceId}/${entity.entityId}`, { environment });
189
223
  const definition = structuredClone((current?.data ?? current));
190
- const resolvedOps = resolvePatchOperations(definition, operations, 'entity_get');
224
+ // Bağ yolu seçici çözümünden ÖNCE çevrilir: '/applicationIds' belgede yoktur (37 S-1).
225
+ const bindingOperations = structuredClone(operations);
226
+ const bindingNotes = normalizeApplicationBindingOps(bindingOperations);
227
+ const resolvedOps = resolvePatchOperations(definition, bindingOperations, 'entity_get');
191
228
  // Politika, yazmadan önce sonucu görmeli; yerel kopya YALNIZ değerlendirme için, gövde olarak gönderilmez.
192
229
  const preview = fastJsonPatch.applyPatch(definition, resolvedOps.operations, true, false).newDocument;
193
230
  const protectionChanges = changedExistingProtections(current?.data ?? current, preview);
@@ -208,7 +245,7 @@ export function registerEntityTools(server, platform) {
208
245
  });
209
246
  const written = patchResult(saved, preview);
210
247
  const touched = patchedSections(written.data, resolvedOps.operations);
211
- return attachFindings(withUnmappedMembers(withResolved({
248
+ return attachFindings(withUnmappedMembers(withResolved(withNormalization({
212
249
  applied: operations.length,
213
250
  newVersion: written.newVersion,
214
251
  patchTargets: resolvedOps.targets,
@@ -216,7 +253,7 @@ export function registerEntityTools(server, platform) {
216
253
  // S-8: yazılanın kanıtı yanıtta dursun; dokunulmayan bölümler outline'da kalır.
217
254
  ...(Object.keys(touched).length > 0 ? { touched } : {}),
218
255
  outline: entityOutline(written.data)
219
- }, resolver.resolved), drift), findings);
256
+ }, bindingNotes), resolver.resolved), drift), findings);
220
257
  }));
221
258
  server.registerTool('entity_protection_update', {
222
259
  title: 'Alan korumasını değiştir',
@@ -3,7 +3,7 @@ import { resolvePatchOperations } from '../knowledge/outlines.js';
3
3
  import { patchResult, baseVersionOf } from '../platform/patch.js';
4
4
  import fastJsonPatch from 'fast-json-patch';
5
5
  import { fillComponentNames, componentNameNote, normalizeComponentDefaults, componentDefaultsNote, flexSizeWarnings, spacingLayerWarnings, verticalGapWarnings, withNormalization } from '../knowledge/normalize.js';
6
- import { fetchHost, layoutUpdateBody, schemaParam, eventsParam, modeParam, summarize, findNode } from './screens.js';
6
+ import { fetchHost, layoutUpdateBody, schemaParam, eventsParam, modeParam, summarize, findNode, readFullSchema } from './screens.js';
7
7
  import { assertScreenWriteGuards, assertScreenCatalog } from './screenWrite.js';
8
8
  import { applyPropTypesToSchema, isSetPropTypeOp, setPropTypeErrors } from '../knowledge/propTypes.js';
9
9
  import { writeDiff, withWriteDiff } from '../platform/writeDiff.js';
@@ -58,24 +58,39 @@ export function registerLayoutTools(server, platform) {
58
58
  }));
59
59
  server.registerTool('layout_schema_get', {
60
60
  title: "Layout komponent ağacını oku",
61
- description: "Layout'un komponent ağacını (views) okur — page_schema_get'in layout karşılığı. Ağaçta hedefli değişiklik için layout_schema_patch (sayfa ile aynı micro-op sözlüğü, PATCH /layouts/{app}/{id}/schema). Ad/parametre/olay alanlarında ya da düz JSON Patch yolu gerektiğinde layout_patch — o araç buradaki updatedAt'i expectedUpdatedAt olarak kabul eder. Tam ağacı kendiniz yazacaksanız layout_update.",
61
+ description: "Layout'un komponent ağacını (views) okur — page_schema_get'in layout karşılığı. Ağaçta hedefli değişiklik için layout_schema_patch (sayfa ile aynı micro-op sözlüğü, PATCH /layouts/{app}/{id}/schema). Ad/parametre/olay alanlarında ya da düz JSON Patch yolu gerektiğinde layout_patch — o araç buradaki updatedAt'i expectedUpdatedAt olarak kabul eder. Tam ağacı kendiniz yazacaksanız layout_update. full ve subtree yanıtı nodeCount taşır; full tek yanıta sığmazsa HATA döner (sessiz kırpma yok): cursor:'0' ile parça parça okuyun — page_schema_get ile aynı sözleşme (items[] { parentElementId, index, node }, nextCursor bitene kadar; layout okuma arasında değişirse 409). allowTruncated:true kırpılmış gövdeyi açıkça verir — o gövdeyi layout_update'e geri yazmayın.",
62
62
  inputSchema: {
63
63
  project: projectParam,
64
64
  environment: environmentParam,
65
65
  applicationId: guid,
66
66
  id: guid,
67
- mode: modeParam,
68
- componentId: z.string().optional().describe("subtree kipinde kök alınacak komponentin elementId'si")
67
+ mode: modeParam.describe("summary (varsayılan): yalnız id/elementId/name/type ağacı — hedef bulmak için bunu kullanın. subtree: componentId ile tek alt ağaç. full: tüm views; tek yanıta sığmazsa HATA döner — cursor:'0' ile parça parça okuyun (nextCursor bitene kadar) ya da allowTruncated:true ile kırpılmış gövdeyi açıkça isteyin."),
68
+ componentId: z.string().optional().describe("subtree kipinde kök alınacak komponentin elementId'si"),
69
+ cursor: z.string().min(1).optional().describe("Yalnız mode:'full': parçalı okuma. İlk çağrıda '0', sonra yanıttaki nextCursor; nextCursor gelmeyince views bitmiştir. Layout okuma arasında değişirse (updatedAt) 409 döner — cursor:'0' ile baştan başlayın."),
70
+ allowTruncated: z.boolean().optional().describe("Yalnız mode:'full': true verilirse sığmayan views hata yerine kırpılarak döner (nodeCount toplamı söyler, düşen düğümler söylenmez). Varsayılan false.")
69
71
  },
70
72
  annotations: readOnly
71
- }, handle(config, async ({ project, environment, applicationId, id, mode, componentId }) => {
73
+ }, handle(config, async ({ project, environment, applicationId, id, mode, componentId, cursor, allowTruncated }) => {
72
74
  const layout = await fetchHost(platform, 'layout', project, environment, applicationId, id);
73
75
  const typeField = applicationTypeField(await resolveProfile(platform, applicationId));
74
76
  const audit = layout.audit;
75
77
  const updatedAt = audit?.updatedAt ?? audit?.createdAt;
76
78
  const views = (layout.views ?? []);
77
- if (mode === 'full')
78
- return { updatedAt, ...typeField, views };
79
+ if (cursor !== undefined && mode !== 'full') {
80
+ throw new PlatformApiError(`cursor yalnız mode:'full' ile çalışır (verilen mode: '${mode ?? 'summary'}'); parçalı okuma tüm views içindir. mode:'full' ile cursor:'0' verin ya da cursor'ı kaldırın.`, 400);
81
+ }
82
+ if (mode === 'full') {
83
+ return readFullSchema({
84
+ noun: 'Layout',
85
+ field: 'views',
86
+ nodes: views,
87
+ versionKey: String(updatedAt ?? ''),
88
+ base: { updatedAt, ...typeField },
89
+ cursor,
90
+ allowTruncated,
91
+ maxResponseBytes: config.maxResponseBytes
92
+ });
93
+ }
79
94
  if (mode === 'subtree') {
80
95
  if (!componentId)
81
96
  throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
@@ -49,6 +49,7 @@ export function buildMobileAppPlan(input) {
49
49
  ];
50
50
  }
51
51
  /** Kabuk ağacı örnek dosyasından okunur — örnek ile aracın kurduğu layout tek kaynaktır. */
52
+ const drawerButtonId = (pageSlug) => `menu_${pageSlug.replace(/[^A-Za-z0-9]+/g, '_')}`;
52
53
  function shellViews(input) {
53
54
  const example = JSON.parse(readKnowledgeFile('examples', input.drawer ? 'mobile-app-shell-drawer.json' : 'mobile-app-shell-stack.json'));
54
55
  const views = structuredClone(example.layoutCreate.views);
@@ -57,6 +58,19 @@ function shellViews(input) {
57
58
  const appBar = findAppBar(views);
58
59
  if (appBar && input.title)
59
60
  appBar.props.title = { type: 'static', ref: null, data: input.title };
61
+ // 37 S-9 / g-24: örneğin düğmeleri ŞABLONDUR; çekmece uygulamanın kendi sayfalarından birer düğme taşır.
62
+ const drawer = views.find(node => node.type === 'drawer');
63
+ const template = drawer?.children?.find(node => node.type === 'button');
64
+ if (drawer && template) {
65
+ drawer.children = input.pages.map(page => {
66
+ const elementId = drawerButtonId(page.pageSlug);
67
+ const button = structuredClone(template);
68
+ button.elementId = elementId;
69
+ button.name = elementId;
70
+ button.props = { ...button.props, text: { type: 'static', ref: null, data: page.pageName } };
71
+ return button;
72
+ });
73
+ }
60
74
  return views;
61
75
  }
62
76
  const SHELL_TYPES = ['app-bar', 'slot'];
@@ -274,7 +288,7 @@ export function registerMobileTools(server, platform) {
274
288
  ...(tabs ? { tabs: tabs.map(tab => tab.pageSlug) } : {}),
275
289
  completed,
276
290
  ...(warnings.length > 0 ? { warnings } : {}),
277
- next: "Ekranları page_schema_patch ile doldurun; doğrulama page_screenshot (mobilde varsayılan device: 'phone'). platform_guide('mobile')."
291
+ next: `Ekranları page_schema_patch ile doldurun; doğrulama page_screenshot (mobilde varsayılan device: 'phone'). ${input.drawer ? `Çekmece düğmeleri sayfalardan türedi (${pages.map(page => drawerButtonId(page.pageSlug)).join(', ')}): her birine flow_create (host: 'layout', redirect) + layout_schema_patch set_event { event: 'click' } bağlayın. ` : ''}platform_guide('mobile').`
278
292
  };
279
293
  }));
280
294
  }