@appaflytech/zeyrek-ai-mcp 0.0.62 → 0.0.63

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 (41) hide show
  1. package/.env.example +6 -0
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/backendConstraints.js +2 -2
  4. package/dist/knowledge/mobileStore.js +24 -4
  5. package/dist/knowledge/normalize.js +10 -0
  6. package/dist/knowledge/platform/mobileLayoutLint.js +8 -0
  7. package/dist/knowledge/platform/mobileRules.js +6 -0
  8. package/dist/knowledge/rules.js +89 -0
  9. package/dist/observability/gapSink.js +43 -0
  10. package/dist/platform/argumentAliases.js +6 -0
  11. package/dist/platform/capture/actions.js +71 -2
  12. package/dist/platform/capture/capture.js +5 -2
  13. package/dist/platform/responses.js +5 -2
  14. package/dist/platform/writeDiff.js +52 -1
  15. package/dist/resources/examples/middleware-password-reset.json +1281 -0
  16. package/dist/resources/examples/mobile-reorder-persist.json +33 -7
  17. package/dist/resources/examples/workflow-email-step.json +67 -0
  18. package/dist/resources/guides/deployment.md +13 -1
  19. package/dist/resources/guides/entity.md +7 -0
  20. package/dist/resources/guides/expressions.md +5 -0
  21. package/dist/resources/guides/getting-started.md +15 -30
  22. package/dist/resources/guides/microservice.md +30 -2
  23. package/dist/resources/guides/middleware.md +83 -3
  24. package/dist/resources/guides/mobile-layout.md +35 -3
  25. package/dist/resources/guides/mobile-publish.md +33 -1
  26. package/dist/resources/guides/mobile-steps.md +12 -2
  27. package/dist/resources/guides/mobile.md +11 -8
  28. package/dist/resources/guides/page.md +28 -3
  29. package/dist/resources/guides/workflow.md +30 -1
  30. package/dist/resources/schema-fingerprint.json +2 -2
  31. package/dist/tools/applications.js +6 -3
  32. package/dist/tools/common.js +8 -0
  33. package/dist/tools/designSchemaPatch.js +4 -1
  34. package/dist/tools/flows.js +14 -4
  35. package/dist/tools/layouts.js +3 -1
  36. package/dist/tools/microservices.js +8 -2
  37. package/dist/tools/mobileStore.js +13 -2
  38. package/dist/tools/pages.js +27 -12
  39. package/dist/tools/platformInfo.js +15 -6
  40. package/dist/tools/workflows.js +4 -2
  41. package/package.json +1 -1
@@ -191,8 +191,8 @@ Workflow'un `file-upload` adımı da aynı nesneyi döndürür, adres `<adım>.d
191
191
 
192
192
  Mobil `repeater`'ın çocukları TAM İKİ `view`'dır (web'de tek şablon) — satır şablonu ve boş görünüm (ADR
193
193
  docs/adr/0005; başka şekil 422 `fixed-views`). Satır yokken boş görünüm bir kez, tam genişlikte
194
- çizilir; boş ekranı oraya tasarla. `emptyTitle`/`emptyDescription`/`emptyIcon` prop'ları ve kardeş
195
- `emptystate` yolu 2026-09-30'da kaldırıldı. Mobil `view` çocuklarını sütunda dizer. Tam gövde:
194
+ çizilir; boş ekranı oraya tasarla. Mobil `view` çocuklarını sütunda dizer.
195
+ `columnCount` > 1'de `horizontalSpacing` hücre payıdır, satır taşmaz. Tam gövde:
196
196
  `ngapps://examples/mobile-list-detail-form`.
197
197
 
198
198
  Sürükle-sırala: `reorderable: true` + `orderField` (yalnız dikey, tek sütun — MOB-035); bırakınca `reorder`
@@ -397,15 +397,15 @@ etiketi ailesini `typographyStyle`'dan alır, alan etiketleri platform yazı tip
397
397
  `badge.ref` **saklı** state yolu; `keep`: ikinci dokunuşta yığın durur.
398
398
  Çubuk düzeyinde iki davranış anahtarı vardır ve **yalnız varsayılandan ayrılınca yazılır**: `"keepStackOnSwitch": false`
399
399
  (sekme değişince hedef sekme köke döner; alan yoksa yığın korunur) ve `"hideOnKeyboard": true` (klavye açılınca çubuk
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.
400
+ gizlenir; `platform_guide('mobile-layout')` §Klavye). `true`/`false`'tan başka değer (ör. metin `"false"`) yok sayılır ve `MOB-007` uyarır.
401
401
  İkinci dokunuş (`reselect`) ile sekme değişimi (`keepStackOnSwitch`) ayrı ayardır; eski runtime anahtarları bilmez ve
402
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
403
+ - **Sekme sayfasına `redirect` sekmeye geçer, kopya itmez** (kabuk 0.12.0+, `tabRedirectReturns`; eskide yığına kopya iter). Hedef (`pageId` ya da sayfanın yolu) bir sekme sayfasıysa
404
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
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
406
+ - **`back` adımı iki kabukta da yığında bir geri gider** (kabuk 0.12.0+, `backStep`; eskide önceki sayfaya `redirect`). Gidemezse (yığının ilk ekranı) hiçbir şey yapmaz, akış yine
407
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
408
+ - **Sayfadan çıkılınca açık akış yarıda kalmaz** (kabuk 0.12.0+, `flowDrain`): telefona yazan adımlar sonuna kadar koşar, ekrana dokunanlar atlanır
409
409
  (`platform_guide('mobile-steps')` §Sayfa kapanınca).
410
410
  - **Çekmece bir bileşendir.** `drawer` widget'ı + `app-bar`'ın `menuTarget` prop'u çekmeceyi açar.
411
411
  Çekmece layout'un kök dizisindedir (`views: [shell_root, nav_drawer]`), kabuk kabının içinde değil.
@@ -484,7 +484,7 @@ Sunucudan gönderilen bildirim yerel `notification-schedule`'dan ayrıdır. Kana
484
484
  ## Kalıcı state
485
485
 
486
486
  State varsayılan olarak **bellektedir**: `state.page` sayfa her açıldığında `variable_create`'teki `value`'dan başlar,
487
- ekran kapanınca kaybolur. **Mobil kabukta `state.app` uygulama + ortam başına tek depodur** (kök ADR 0031): bir
487
+ ekran kapanınca kaybolur. **Mobil kabukta `state.app` uygulama + ortam başına tek depodur** (kök ADR 0031; kabuk 0.12.0+, `sharedAppState` — eskide her sayfa kendi kopyasını alır): bir
488
488
  sayfanın yazdığı arkadaki sayfada da anında görünür; açılan sayfanın `value`'su yalnız depoda olmayan anahtarı
489
489
  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).
490
490
  Uygulama kapanıp açıldığında kaldığı yerden sürmesi gereken değer için değişkene saklama türü verin:
@@ -532,6 +532,8 @@ variable_create({ applicationId, pageId, variables: [
532
532
  ö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
533
533
  (liste katmanda açılır, `optionsSeen` kaç seçenek görüldüğünü söyler); `tab` bir `tabs`
534
534
  bileşeninin sekmesini anahtarıyla (`<elementId>__tab-<anahtar>`) bulur.
535
+ - **`click` gerçek noktaya basar** (SCR-024): o noktada üstte başka öğe varsa `applied: false` + `coveredBy` —
536
+ `platform_guide('mobile-steps')` §Önizlemede çekim hedefleri.
535
537
  - `flowRuns: 0` + `requests: 0` bir tıklamada akışın koşmadığını söyler: olay bağını
536
538
  `page_runtime_get`, akışı `flow_get` ile doğrulayın.
537
539
  - Kuru doğrulama: `page_schema_validate` mobil kataloğa karşı doğrular (`MOB-003`).
@@ -596,7 +598,8 @@ yaptıkları. Derlenen uygulama
596
598
  - Ağ yokken ekranlar ve (açıksa) son okunan veri cihazdaki kopyadan gelir: `properties.mobile.offline`
597
599
  (MOB-033), `Steps.<ad>.cache.at`, `error.network` — `platform_guide('page')` §Kural 1g-4. Önizleme önbellek kullanmaz.
598
600
  Yalnız GET okuma saklanır (POST okuma ağsız açılmaz → GET + query-string, FLOW-044); kuyruk değişimi sayfanın
599
- `offlineQueueChange` olayıdır, yoklama gerekmez (§Kural 1g-4).
601
+ `offlineQueueChange` olayıdır, yoklama gerekmez (§Kural 1g-4; kabuk 0.12.0+, `offlineQueueEvent`). Hangi özelliğin
602
+ hangi kabukta olduğu: `platform_guide('mobile-publish')` §Kabuk sürümleri.
600
603
  Uygulama kaydı (ad, slug, `properties`, ana sayfa, bulunamadı sayfası) `application_update` ile yalnız
601
604
  **development**'a yazılır; production'da `deployment_start`'tan sonra geçerli olur (APP-001) — önce development'ta
602
605
  deneyin. Slug değişirse production derlemesi dağıtıma kadar eski slug'ı taşır.
@@ -1556,7 +1556,7 @@ Kayıtlar kullanıcıya, ortama ve veri diline bağlıdır; çıkışta ve kulla
1556
1556
  - **Görünürlük:** kaydın sonucu akışa dönmez. `Device.offline.queue = { pending, rejected }` ve
1557
1557
  `Device.offline.rejected` (`id`, `service`, `at`, `status`, `code`, `message`); kod adımında `Page.offline.list()`,
1558
1558
  `.count()`, `.sync()`, `.resolve(id, 'retry' | 'discard')`. Süresi dolan kayıt yeniden denenmez.
1559
- - **Kuyruk değişince ekran tazelenir:** mobil sayfanın `offlineQueueChange` olayı (`page.events`, `mount`/`destroy`/`focus`
1559
+ - **Kuyruk değişince ekran tazelenir** (kabuk 0.12.0+, `offlineQueueEvent`; eskide olay koşmaz, listeyi bir zamanlayıcıyla yeniden okuyun): mobil sayfanın `offlineQueueChange` olayı (`page.events`, `mount`/`destroy`/`focus`
1560
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
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
1562
  koşarken kuyruğun değişmesi olayı yeniden tetiklemez (kuyruğa yazan akış döngüye girmez). Listeyi bekleyenlerle
@@ -1567,6 +1567,22 @@ Kayıtlar kullanıcıya, ortama ve veri diline bağlıdır; çıkışta ve kulla
1567
1567
 
1568
1568
  Örnek: `ngapps://examples/mobile-offline-queue` (doz işaretleme: servis bildirimi + `queued` dalı).
1569
1569
 
1570
+ **Hangi dal koşar** (`microservice` adımı; sıra kök ADR 0004, `ng-apps-flow` `microservice.ts`). Her hücre koşan
1571
+ TEK yoldur; `always` yalnız "always" yazan hücrede koşar:
1572
+
1573
+ | Durum | `error` dalı bağlı | `error` yok, akışta `errorSteps` var | ikisi de yok |
1574
+ |---|---|---|---|
1575
+ | Çevrimiçi başarı | `always` | `always` | `always` |
1576
+ | Çevrimiçi ret (4xx/5xx; bildirimsiz servise `queued` bağlı yazma da 422) | `error` — **`always` koşmaz** | `errorSteps` | `always` (`Steps.<ad>.data` `null`) |
1577
+ | Ağ yok + `queued` bağlı + mobil + yazma (POST/PUT/DELETE) + dosyasız | `queued` | `queued` | `queued` |
1578
+ | Ağ yok, ötekiler (web, önizleme, GET, `queued` yok) | `error` | `errorSteps` | `always` (`data` `null`) |
1579
+ | Ağ yok + `queued` bağlı ama dosya parametresi var | `error` (kod `SERVICE_CALL_FILE_NOT_QUEUED`) | `errorSteps` | `always` |
1580
+
1581
+ `success` dalı yalnız eski şemada vardır; bağlıysa başarıda `always` yerine o koşar, `error` ve `errorSteps` yokken
1582
+ hatada da o koşar. Hata yanıtı `uiActions` taşıyorsa (sunucunun döndürdüğü arayüz eylemleri) o eylemler koşar ve adım
1583
+ hiçbir dala geçmez. `error` bağlıyken ortak son adımı
1584
+ (`txt_sonuc`, yükleme kapatma) `error` dalına da bağlayın: hata `always`'e hiç uğramaz.
1585
+
1570
1586
  **Veri önbelleği açıkken akış ne görür:**
1571
1587
 
1572
1588
  - Saklanan okuma 8 sn süre sınırı taşır; yanıt gelirse olduğu gibi döner ve arkada saklanır. Saklanmayan isteklerde
@@ -1603,6 +1619,8 @@ AYNI; veri bağlantı gelince Zeyrek'e kendiliğinden gider. Altı adım birlikt
1603
1619
  2. **Okuma GET:** okuyan uç `httpMethod: 'get'`, adım `isRaw: false`; uç filtreleri ve adım parametreleri
1604
1620
  `source: 'query-string'` (GET'te gövde gitmez). Önbellek yalnız GET okumayı saklar; POST ile okuyan adım ağsızken
1605
1621
  kayıttan dönemez ve hata yoluna düşer — **FLOW-044** uyarır (yöntem servis tanımındandır, adıma yazılan saklanmaz).
1622
+ İş akışı kaynaklı uçta gateway okuma mı yazma mı bilemez; POST ile gidilirse yalnız bilgi notu düşer (uç
1623
+ `offlineWritable` ya da `queued` dalı bağlıysa o da düşmez), okuyan akışın ucunu GET yapmak sizin işinizdir.
1606
1624
  (Ölçüm: POST okuyan bir uygulamanın listesi ağsızken "yüklenemedi" dedi.)
1607
1625
  3. **Yazma kuyruğu, uyarısız:** yazan uç `offlineWritable: true`; `microservice` adımının `queued` dalı `always` ile
1608
1626
  **aynı adıma** bağlanır — ağsız yazma da çevrimiçi gibi biter. Adımın `error` dalı çevrimiçi reddi uyarır; kuyruktaki
@@ -1614,7 +1632,8 @@ AYNI; veri bağlantı gelince Zeyrek'e kendiliğinden gider. Altı adım birlikt
1614
1632
  `offlineQueueChange` olayına da bağlanır: kuyruğa giren kayıt listeye hemen, gönderilen kayıt sunucudan gelir.
1615
1633
  Kaydı yazan sayfadan çıkılsa da yazma akışı tamamlanır (kök ADR 0030) ve `state.app` mobil kabukta sayfalar arası
1616
1634
  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).
1635
+ kabuk 0.12.0+'da gerekmez (`flowDrain`, `sharedAppState`, `offlineQueueEvent`; eski kabukta kalır — yayındaki sürüm
1636
+ `mobile_store_get` `shell`; Defter saha raporu, 2026-10-30).
1618
1637
  5. **Düğme kilidi:** yazan düğme istek süresince `isLoading` + `isDisabled`; her dalın sonunda açılır. Kilitsiz düğmede
1619
1638
  hızlı ikinci dokunuş yeni bir istek ve yeni bir `Idempotency-Key`'dir — sunucu ikisini de uygular (ölçüm: 1,6 sn'de
1620
1639
  üç dokunuş, üç kayıt).
@@ -1660,7 +1679,11 @@ kaydedilmemiş sırada kalır. Hatayı kullanıcıya gösterin; eski sıraya dö
1660
1679
  1. Uç: tablo kaynaklı `operation: 'bulk-update'`, `fields` = birincil anahtar + sıra alanı (başka alan koymayın —
1661
1680
  tanımdaki alan gövdede yoksa NULL yazılır, `platform_guide('microservice')` §Güncelleme sözleşmesi). Gövdedeki
1662
1681
  anahtarlar alan **slug'larıdır**; `fields[].name` ile yeniden adlandırma yoktur (platform sessizce düşürür), yani
1663
- `{ id, value }` yaması uca olduğu gibi gitmez, eşlenir.
1682
+ `{ id, value }` yaması uca olduğu gibi gitmez, eşlenir. **Uç sahiplikli kurulur:** `middlewareIds` ile oturum ister
1683
+ (`platform_guide('middleware')` Tarif 1) ve `filters`'ta `{ field: <sahip alanı>, operator: 'eq', source: 'expression',
1684
+ value: 'user.id' }` taşır. Platform bu kapsamı her satırın `UPDATE`'ine koyar; başkasının satır kimliği gelirse bütün
1685
+ parti 404 ile reddedilir, hiçbir satır yazılmaz (MS-027, `platform_guide('microservice')` §Toplu yazmada sahiplik).
1686
+ Filtresiz uç gönderilen her kimliği yazar.
1664
1687
  2. Akış adımı: ucun gövde parametrelerinden **yalnız birine** (birincil anahtarınkine, ör. `id`) satır listesini
1665
1688
  `code` ile verin, öbürlerini boş bırakın. Servisin şeması `isParameterArray` dediği için değeri olan tek gövde
1666
1689
  parametresinin dizisi gövdenin KENDİSİ olur — parametrenin adı anlamını yitirir:
@@ -2516,6 +2539,8 @@ start → [setter] btn.isLoading = true, btn.isDisabled = true, notification giz
2516
2539
  - Prop değeri nesne olabilir: `icon` → `{ "type": "static", "ref": { "name": "progress_activity" } }`.
2517
2540
  - Giriş sayfası kimlik işinin görünen yarısıdır. Sign-in ucu + auth middleware'i kurup bu sayfayı
2518
2541
  kurmamak, sistemi kullanılamaz bırakır.
2542
+ - "Şifremi unuttum" bağlantısı anonim bir sıfırlama sayfasına gider; uçlar ve sayfa akışı
2543
+ `platform_guide('middleware')` §Tarif 4.
2519
2544
 
2520
2545
  ### 7d — Detay / profil sayfası
2521
2546
 
@@ -116,7 +116,9 @@ Aktif dil `context.language`, anahtar çözmek için `Loc(key)`. Motor kültür
116
116
  ## Step'ler nasıl bağlanır
117
117
 
118
118
  Her step'in `branches[]` dizisi vardır; `nextStepId` sonraki step'in id'sini gösterir.
119
- Bağlanmamış step çalışmaz. `start` tek branch taşır; normal step'ler `success` + `fail`;
119
+ Bağlanmamış step çalışmaz. `start` tek branch taşır ve o dalın **adı** `start`, **tipi** `success`'tir:
120
+ `{ name: 'start', type: 'success', nextStepId }` — `type: 'start'` platformda 400 alır (dal tipi yalnız `success` ·
121
+ `fail` · `impersonation` · `global-error`; WF-038). Rehberlerdeki `start → …` okları bu dalı gösterir. Normal step'ler `success` + `fail`;
120
122
  `condition` koşul branch'leri + `else`; terminal step'ler (`response`, `convert`, `revert`)
121
123
  **branch taşımaz**. Ayrı bir `end` step tipi yoktur; son dalın `nextStepId` değerini
122
124
  `null` bırakarak akışı bitirin.
@@ -426,6 +428,33 @@ Workflow ya da otomasyon, olay anında BELİRLİ uygulama kullanıcılarına (ya
426
428
  `automation_trigger` ile çalıştıramaz — istek gitmeden reddedilir. Development'ta deneyin.
427
429
  - `to.users` GUID olmalı; uygulama `applicationId` ile verilirse o da mobil olmalı (değilse 404, hata dalı).
428
430
 
431
+ ## Email adımı: alıcı yalnız `rule.expression` (WF-037)
432
+
433
+ E-posta adımı platformun SMTP ayarıyla (lokal yığında `mailpit`, arayüz `http://localhost:8025`) ileti gönderir.
434
+ Gönderen adresi platform ayarıdır; adım yalnız gönderen **adını** verir.
435
+
436
+ | Kural anahtarı | Ne | Örnek |
437
+ |---|---|---|
438
+ | `expression` | Alıcılar: `{ to, cc, bcc }` adres dizgisi listesi döndüren ifade. `cc`/`bcc` isteğe bağlı | `"({ to: [input.email] })"` |
439
+ | `subject` | Konu ifadesi | `"'Şifre sıfırlama kodu'"` |
440
+ | `body` | Gövde ifadesi; HTML olarak gider | `` "`<p>Kodunuz: <b>${kod.data}</b></p>`" `` |
441
+ | `displayName` | Gönderen adı | `"'Defter'"` |
442
+ | `attachment` | Base64 dizgi listesi döndüren ifade (isteğe bağlı) | `"[]"` |
443
+
444
+ - **Alıcı başka bir anahtardan okunmaz.** `send`, `send.toMail`, `to`, `toMail`, `sendTo`, `recipients` kural
445
+ düzeyinde yazılırsa platform bağlarken atar; gateway kaydı WF-037 ile durdurur. `workflow_step_schema`'daki
446
+ `expression.to|cc|bcc` bu ifadenin **döndürdüğü şekildir** (2026-11-04'ten önce şema bunu `send.toMail`/`send.to`
447
+ diye kural anahtarı gibi gösteriyordu — zeyrek-backend incident #197).
448
+ - Liste öğesi adres **dizgisidir**; boş öğe (`null`, `undefined`, `''`) atlanır, böylece `cc: [row.managerEmail]`
449
+ yöneticisiz kayıtta yalnız `to`'ya gider. `{ type, value }` gibi nesne öğe adımı `EmailRecipientInvalidException` ile,
450
+ hiç alıcı olmaması `EmailRecipientsMissingException` ile **fail** dalına düşürür (ileti gönderilmez). Eski
451
+ `({ toMail: '…' })` / `bccMails` biçimi hâlâ okunur, yeni akışta kullanmayın.
452
+ - SMTP hatası da fail dalıdır; fail dalı bağlı değilse akışın global hata dalına gider.
453
+ - **Önizleme gerçekten gönderir** (Push'tan farkı): `workflow_preview` ile denediğiniz alıcıya ileti gider. Lokal
454
+ yığında `curl -s localhost:8025/api/v1/messages` son iletileri döner.
455
+ - Ölçülmüş örnek: `ngapps://examples/workflow-email-step`. Parola sıfırlama tarifi: `platform_guide('middleware')`
456
+ §Tarif 4.
457
+
429
458
  ## Step tipi × workflow tipi izin matrisi
430
459
 
431
460
  | Step | workflow | automation | middleware |
@@ -1,6 +1,6 @@
1
1
  {
2
- "hash": "68157be9624742c7086f9e40a9c8fb5565d612d82bf0a7d7d1ae063044ac7465",
3
- "generatedAt": "2026-11-03T19:09:26.033Z",
2
+ "hash": "88ccb2c27599cdeb3701f0e4546adee3c3845bd3b7c32c1d5a49b254c70e47a3",
3
+ "generatedAt": "2026-11-04T11:15:39.446Z",
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.",
@@ -141,11 +141,14 @@ export function registerApplicationTools(server, platform) {
141
141
  }));
142
142
  server.registerTool('application_delete', {
143
143
  title: 'Application sil',
144
- description: `Application'ı kalıcı olarak siler — development'tan (ve Core'dan). ORTAM (b-17, APP-001): production'daki kopya ve kullanıcı verisi deployment_start'a kadar yerinde kalır, silme dağıtımla gider; dallar kendi kopyasını tutar. ${templateSubscriptionLock}`,
145
- inputSchema: { id: guid },
144
+ description: `Application'ı kalıcı olarak siler — development'tan (ve Core'dan). ORTAM (b-17, APP-001): production'daki kopya ve kullanıcı verisi deployment_start'a kadar yerinde kalır, silme dağıtımla gider; dallar kendi kopyasını tutar. SON UYGULAMA (PS-17): varsayılan olmayan projenin tek uygulaması silinirse PROJE DE SİLİNİR — teması, tabloları, verisi ve veritabanıyla; platform bunu deleteProject: true olmadan 409 LastApplicationDeleteRequiresConfirmationException ile reddeder. deleteProject'i yalnız kullanıcı projenin de gitmesini açıkça istediyse verin. ${templateSubscriptionLock}`,
145
+ inputSchema: {
146
+ id: guid,
147
+ deleteProject: z.boolean().optional().describe('Projenin son uygulamasını silerken projenin de silinmesine açık onay. Yalnız kullanıcı istediyse true; başka durumda platform yok sayar.')
148
+ },
146
149
  annotations: destructive
147
150
  }, handle(config, async (args) => {
148
- const response = await platform.org('DELETE', `/applications/${args.id}`);
151
+ const response = await platform.org('DELETE', `/applications/${args.id}`, args.deleteProject === true ? { query: { deleteProject: true } } : {});
149
152
  forgetApplication(platform, args.id);
150
153
  return response;
151
154
  }));
@@ -90,3 +90,11 @@ export const readOnly = { readOnlyHint: true, openWorldHint: false };
90
90
  export const writes = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
91
91
  export const updates = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
92
92
  export const destructive = { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false };
93
+ /** A patch whose net diff against the stored record is empty: nothing is sent (the platform rejects empty `ops` with 400, PS-28). */
94
+ export function unchangedPatch(applied) {
95
+ return {
96
+ outcome: 'unchanged',
97
+ applied,
98
+ note: "değişiklik yok — gönderilen op'lar kayıtlı tanımı değiştirmiyor; platforma istek gönderilmedi"
99
+ };
100
+ }
@@ -1,4 +1,4 @@
1
- import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEventOps, stripGatewayEventFields, staleSetterWarnings, normalizeSchemaOps, assertAddComponentIds, assertNoRetiredTypes, validationNote, componentNameNote, componentDefaultsNote, replacePropNote, flexSizeWarnings, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, verticalGapWarnings, withNormalization, FLEX_LAYOUT_PROP_NAMES } from '../knowledge/normalize.js';
1
+ import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEventOps, flowIdEventOps, stripGatewayEventFields, staleSetterWarnings, normalizeSchemaOps, assertAddComponentIds, assertNoRetiredTypes, validationNote, componentNameNote, componentDefaultsNote, replacePropNote, flexSizeWarnings, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, verticalGapWarnings, withNormalization, FLEX_LAYOUT_PROP_NAMES } from '../knowledge/normalize.js';
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';
@@ -107,6 +107,9 @@ export function registerDesignSchemaPatchTool(server, platform, host) {
107
107
  const connectionIssues = [...opRecords, ...propTypeOps].flatMap(entry => connectDataOpIssues(entry, connectedProps).map(issue => `${issue.code} — ${issue.message}`));
108
108
  if (connectionIssues.length > 0)
109
109
  throw new PlatformApiError(`Connect Data op gövdesi geçersiz (istek gönderilmedi): ${connectionIssues.join(' · ')}`, 400);
110
+ const flowIdOps = flowIdEventOps(opRecords);
111
+ if (flowIdOps.length > 0)
112
+ throw new PlatformApiError(`PAGE-052 — olay akışı eventValue ile alır (istek gönderilmedi): ${flowIdOps.join(' · ')}`, 400);
110
113
  if (opRecords.some(entry => entry.op === 'set_event' || entry.op === 'add_event')) {
111
114
  const bound = boundEventOps(nodes, opRecords);
112
115
  if (bound.length > 0)
@@ -1,5 +1,5 @@
1
1
  import { fetchHost, flowPath, hostParam, unwrap } from './screens.js';
2
- import { guid, projectParam, environmentParam, readOnly, writes, updates, destructive, microOps } from './common.js';
2
+ import { guid, projectParam, environmentParam, readOnly, writes, updates, destructive, microOps, unchangedPatch } from './common.js';
3
3
  import { applyPolicyMode, evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/policy.js';
4
4
  import { connectedSetterFindings } from '../knowledge/connectDataDefinition.js';
5
5
  import { flowBindings, normalizeFlowSteps, targetlessSetterWarnings, unknownSetterTargetWarnings, withNormalization } from '../knowledge/normalize.js';
@@ -390,7 +390,8 @@ async function queuedBranchWarnings(platform, applicationId, steps, errorSteps,
390
390
  * yöntemle giden mobil adım ağsızken kayıttan dönemez, hata yoluna düşer. Yöntem servis tanımındandır: backend adımın
391
391
  * `httpMethod`'unu her okumada servisten üretir (`StepManagementSchema`), adıma yazılan saklanmaz. Veri önbelleği kapalı
392
392
  * uygulamada hiçbir okuma saklanmadığı için susar. İş akışı ve sorgu kaynaklı uç okuma mı yazma mı bilinmez (kök ADR 0022
393
- * Y1) — susar; okunamayan uygulama ya da servis de uyarı üretmez.
393
+ * Y1): uyarı üretmez; iş akışı kaynaklı uca GET dışı yöntemle giden adım yalnız bilgi notu alır (PS-31) — uç `offlineWritable`
394
+ * ise ya da adımın `queued` dalı bağlıysa yazmadır, not da düşmez; sorgu kaynaklı uç susar. Okunamayan uygulama ya da servis de not üretmez.
394
395
  */
395
396
  async function uncachedReadWarnings(platform, applicationId, steps, errorSteps, readService) {
396
397
  const serviceSteps = [...(Array.isArray(steps) ? steps : []), ...(Array.isArray(errorSteps) ? errorSteps : [])].filter(isRecord)
@@ -409,11 +410,18 @@ async function uncachedReadWarnings(platform, applicationId, steps, errorSteps,
409
410
  continue;
410
411
  const source = String(definition.source ?? '').toLowerCase();
411
412
  const operation = String(definition.sourceParameters?.operation ?? '').toLowerCase();
412
- if (source !== 'view' && !(source === 'table' && (operation === 'get' || operation === 'count')))
413
- continue;
414
413
  const method = String(definition.httpMethod ?? 'get').toLowerCase();
415
414
  if (method === 'get')
416
415
  continue;
416
+ if (source === 'workflow') {
417
+ const queued = (Array.isArray(step.branches) ? step.branches : []).filter(isRecord).some(branch => String(branch.name ?? '') === 'queued' && Boolean(branch.nextStepId));
418
+ if (definition.offlineWritable === true || queued)
419
+ continue;
420
+ warnings.push(`FLOW-044 (bilgi): '${String(step.stepId ?? step.name ?? '?')}' adımı iş akışı kaynaklı '${String(definition.slug ?? args.microserviceId)}' ucuna ${method.toUpperCase()} ile gidiyor — akışın okuduğu mu yazdığı mı tanımdan bilinmiyor; okuyan bir uçsa GET yapın: veri önbelleği yalnız GET okumayı saklar, ağsızken yalnız GET kayıttan döner. Yazan uçsa bu not geçersizdir.`);
421
+ continue;
422
+ }
423
+ if (source !== 'view' && !(source === 'table' && (operation === 'get' || operation === 'count')))
424
+ continue;
417
425
  warnings.push(`FLOW-044: '${String(step.stepId ?? step.name ?? '?')}' adımı '${String(definition.slug ?? args.microserviceId)}' servisinden ${method.toUpperCase()} ile okuyor — veri önbelleği yalnız GET okumayı saklar: ağsızken bu adım kayıttan dönemez, hata yoluna düşer. Okuyan ucu GET yapın (microservice_patch '/httpMethod' 'get'; adımın yöntemi servisten gelir) ve gövdeden okunan parametreleri query-string'e taşıyın — uç filtrelerinin ve adım parametrelerinin source'u 'query-string' olmalı, GET'te gövde gitmez.`);
418
426
  }
419
427
  return warnings;
@@ -794,6 +802,8 @@ export function registerFlowTools(server, platform) {
794
802
  ];
795
803
  // Gövde değil FARK gönderilir; platform PATCH ucu akışı tek başına yazar. Net diff okunan kayıt ile son hâl arasından hesaplanır.
796
804
  const netOps = fastJsonPatch.compare(current, flow);
805
+ if (netOps.length === 0)
806
+ return attachFindings(unchangedPatch(operations.length), [...findings, ...purchaseFindings]);
797
807
  const saved = await platform.tenant('PATCH', project, flowPath(host, applicationId, hostId, flowId), {
798
808
  environment,
799
809
  body: { baseVersion: baseVersionOf(current, 'flow_get'), ops: netOps }
@@ -1,4 +1,4 @@
1
- import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive, microOps } from './common.js';
1
+ import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive, microOps, unchangedPatch } from './common.js';
2
2
  import { resolvePatchOperations } from '../knowledge/outlines.js';
3
3
  import { patchResult, baseVersionOf } from '../platform/patch.js';
4
4
  import fastJsonPatch from 'fast-json-patch';
@@ -259,6 +259,8 @@ export function registerLayoutTools(server, platform) {
259
259
  // kuralları, value'suz olayların elenmesi) son hâle işlendikten sonra net diff hesaplanır.
260
260
  const finalDocument = { ...patched, name: body.name, parameters: body.parameters, views: body.views, events: body.events };
261
261
  const netOps = fastJsonPatch.compare(original, finalDocument);
262
+ if (netOps.length === 0)
263
+ return attachFindings(unchangedPatch(operations.length), textFindings);
262
264
  const saved = await platform.tenant('PATCH', project, `/layouts/${applicationId}/${id}`, {
263
265
  environment,
264
266
  body: { baseVersion: baseVersionOf(document, 'layout_get'), ops: netOps }
@@ -1,4 +1,4 @@
1
- import { guid, appRef, withResolved, projectParam, environmentParam, definitionParam, languageParam, pagingParams, asUserIdParam, readOnly, writes, updates, destructive, microOps } from './common.js';
1
+ import { guid, appRef, withResolved, projectParam, environmentParam, definitionParam, languageParam, pagingParams, asUserIdParam, readOnly, writes, updates, destructive, microOps, unchangedPatch } from './common.js';
2
2
  import { languageHeader } from '../knowledge/languageCodes.js';
3
3
  import { hydrateMicroserviceRefs } from '../knowledge/hydrate.js';
4
4
  import { RefResolver } from '../platform/refs.js';
@@ -449,6 +449,8 @@ export function registerMicroserviceTools(server, platform) {
449
449
  // Gövde değil FARK gönderilir: gateway normalizasyonları da op'a çevrilsin diye net diff
450
450
  // okunan kayıt ile son hâl arasından hesaplanır (tam PUT yok, yalnız değişen alanlar yazılır).
451
451
  const netOps = fastJsonPatch.compare(definition, patched);
452
+ if (netOps.length === 0)
453
+ return attachFindings(unchangedPatch(operations.length), findings);
452
454
  const saved = await platform.tenant('PATCH', project, `/microservices/${resolvedApplicationId}/${id}`, {
453
455
  environment,
454
456
  body: { baseVersion: baseVersionOf(definition, 'microservice_get'), ops: netOps }
@@ -489,7 +491,11 @@ export function registerMicroserviceTools(server, platform) {
489
491
  const bound = bindCallParameters(method, parameters, query, body, notes);
490
492
  assertFilterOperators('microservice_execute', bound);
491
493
  const raw = await platform.tenantRaw(method, project, executePath('m', application, microservice, routeParams), { environment, query: bound.query, body: bound.body, headers: requestHeaders, anonymous, mirrorAuthorizationCookie: true, noNetworkRetry: true });
492
- return withNormalization({ ...raw, headers: headersForMode(raw.headers, includeHeaders ?? 'essential') }, notes);
494
+ return withNormalization({
495
+ ...raw,
496
+ headers: headersForMode(raw.headers, includeHeaders ?? 'essential'),
497
+ truncationHint: "uç tablo/görünüm kaynaklıysa query ile sayfalayın — sayfa boyu ve başlangıç parametrelerinin adı uç tanımının sourceParameters.limit alanındadır (varsayılan limit ve offset; microservice_get ile bakın), toplam satırı total-count başlığı verir; uç başka kaynaklıysa filtreyi daraltın; bütün tablo gerekiyorsa row_export."
498
+ }, notes);
493
499
  }));
494
500
  server.registerTool('microservice_debug', {
495
501
  title: 'Microservice debug',
@@ -12,7 +12,7 @@ import { LAYOUT_IDS } from '../platform/storeArt/layouts.js';
12
12
  import { STYLE_IDS } from '../platform/storeArt/styles.js';
13
13
  import { attachFindings, enforcePolicy, evaluatePolicy } from '../knowledge/policy.js';
14
14
  import { policyRules } from '../knowledge/rules.js';
15
- import { APPLE_RELEASE_TYPES, ASSET_KINDS, BUILD_ASSET_KINDS, COLOR_PATTERN, DECLARATION_KEYS, DEVICE_CLASSES, LOCALE_PATTERN, MAX_ASSET_BYTES, MAX_IMAGES_PER_SLOT, ORIENTATIONS, PLAY_TRACKS, RELEASE_STATUSES, SCREENSHOT_CAPTURE_DEVICES, SPLASH_RESIZE_MODES, USER_INTERFACE_STYLES, adsStoreWarnings, trackingDeclarationWarnings, assetSlotProblem, changedStoreFields, mergeStoreSettings, permissionLaunchShellWarnings, permissionLockShellWarnings, permissionStoreBuildWarnings } from '../knowledge/mobileStore.js';
15
+ import { APPLE_RELEASE_TYPES, ASSET_KINDS, BUILD_ASSET_KINDS, COLOR_PATTERN, DECLARATION_KEYS, DEVICE_CLASSES, LOCALE_PATTERN, MAX_ASSET_BYTES, MAX_IMAGES_PER_SLOT, ORIENTATIONS, PLAY_TRACKS, RELEASE_STATUSES, SCREENSHOT_CAPTURE_DEVICES, SPLASH_RESIZE_MODES, USER_INTERFACE_STYLES, adsStoreWarnings, shellCapabilityView, trackingDeclarationWarnings, assetSlotProblem, changedStoreFields, mergeStoreSettings, permissionLaunchShellWarnings, permissionLockShellWarnings, permissionStoreBuildWarnings } from '../knowledge/mobileStore.js';
16
16
  /**
17
17
  * Mobil mağaza yayınının ajan yolu (`plans/mobil-magaza-yayini` g-11, `plans/mobil-yayin-mcp` g-01):
18
18
  * gizli olmayan mağaza ayarı, mağaza görselleri, beyanlar ve derleme işi. Gizli bilgi (Apple/Google
@@ -388,7 +388,7 @@ export function registerMobileStoreTools(server, platform, storeArt = {}) {
388
388
  const galleryServer = storeArt.gallery ?? sharedGalleryServer;
389
389
  server.registerTool('mobile_store_get', {
390
390
  title: 'Mobil mağaza ayarını oku',
391
- description: "Mobil uygulamanın gizli olmayan mağaza ayarını döner (GET mobile-store/{appId}): kimlik (bundleIdentifier, androidPackage, scheme), sürüm ve sayaçlar, görünüm, izin metinleri, kipleri ve açılışta sorulanlar (permissions, permissionModes, launchPermissions), listing (dil başına mağaza metni), urls, apple, play, review; lockedFields (ilk derlemeden sonra değişmeyen alanlar), görsel yuvası başına sayı (assets: 'screenshot/tr/iphone69': 3) ve beyanlar (declarations: anahtar → işaretli mi); settings.ads reklam ayarıdır (kök ADR 0012: enabled, App ID'ler, testMode, consentForm, appOpenUnit, units[] = adBanner.unit ve ad-show.value'nun andığı yerleşimler) ve reklam açıkken adsWarnings döner (MOB-026: test kipi, platformda birimsiz yerleşim, konsol beyanları); settings.appLock uygulama kilididir (kök ADR 0021: enabled, timeoutSeconds 0·60·300·900). Kayıt hiç yazılmamışsa persisted:false ve VARSAYILANLAR döner (kaydedilmez). manifest:true derlemeye gidecek manifesti de ekler (manifest-preview; sayaçlar SIRADAKİ numara); üretilemiyorsa manifestError alan adıyla döner. detail ayrıntı bölümleri ekler: 'assets' görselleri id'li liste olarak döner (mobile_asset_update / mobile_asset_delete id ister), 'signing' uygulamanın kimlik bilgilerini (signing.app: iOS profili, Android keystore, harita anahtarı, demo hesabı) ve organizasyonun Apple/Google hesaplarını + sertifikalarını (signing.accounts), 'runners' çevrimiçi runner'ları, 'audit' son 20 gizli bilgi olayını (subject: hangi tür, kim, ne zaman, hangi iş), 'permissions' izin görünümünü (permissions.permissions[]: uygulamanın isteyebileceği her izin için mode, text/defaultText, usage — izni kullanan sayfa·bileşen ya da akış adımı (setter ile yazılan tetikleyici prop dahil), ilk 20 yer + usageCount, çağrının environment'ından; usageReadable false ise o ortam okunamamıştır ve boş usage 'kullanılmıyor' demek değildir —, build — mağaza derlemesi taşıyor mu ve neden; HER ZAMAN production'dan: build.usageCount production'daki kullanım, build.staticUsageCount onun yazılı (ifadesiz) kısmı —, devices — aktif cihazların izin durumu sayıları; missingFromStoreBuilds — o izni taşımayan son mağaza derlemeleri; shell.carriesPermissions false ise kipler ve kabuğun henüz taşımadığı izinler bir sonraki kabuk sürümüyle etkili olur; shell.carriesLaunchPermissions false ise açılışta sorulan izinler (askOnLaunch) o kabukta sorulmaz, shell.carriesAppLock false ise açık uygulama kilidi o kabukta yoktur — permissionWarnings söyler), 'status' mağaza durumunu (GET store-status: apple { state, appStoreState, versionString, releaseType, phased* }, play.tracks[] { track, status, versionCodes, rolloutPercent }, actions[] { platform, target, track, irreversible, consequence } — Studio › Yayınla'daki yayın düğmeleri; irreversible:true geri alınamaz, eylemi MCP başlatmaz, kullanıcıya consequence'ı söyleyip Studio'ya yönlendirin), 'changes' son production dağıtımından beri değişenleri iki kovada (GET changes-since-production: live — dağıtımla telefonlara gider, mağaza gerekmez; store — izin/kimlik/görsel gibi yeni mağaza sürümü isteyenler). Gizli DEĞER hiçbir bölümde yoktur — yalnız tür, maskeli id ve meta (ekip, son kullanma); bir bölüm okunamazsa (uç hatası) <bölüm>Error döner, çağrı düşmez. Gizli bilgiyi EKLEMEK/DEĞİŞTİRMEK MCP'den yapılmaz — Studio › Yayın › Hesaplar.",
391
+ description: "Mobil uygulamanın gizli olmayan mağaza ayarını döner (GET mobile-store/{appId}): kimlik (bundleIdentifier, androidPackage, scheme), sürüm ve sayaçlar, görünüm, izin metinleri, kipleri ve açılışta sorulanlar (permissions, permissionModes, launchPermissions), listing (dil başına mağaza metni), urls, apple, play, review; lockedFields (ilk derlemeden sonra değişmeyen alanlar), görsel yuvası başına sayı (assets: 'screenshot/tr/iphone69': 3) ve beyanlar (declarations: anahtar → işaretli mi); settings.ads reklam ayarıdır (kök ADR 0012: enabled, App ID'ler, testMode, consentForm, appOpenUnit, units[] = adBanner.unit ve ad-show.value'nun andığı yerleşimler) ve reklam açıkken adsWarnings döner (MOB-026: test kipi, platformda birimsiz yerleşim, konsol beyanları); settings.appLock uygulama kilididir (kök ADR 0021: enabled, timeoutSeconds 0·60·300·900). Kayıt hiç yazılmamışsa persisted:false ve VARSAYILANLAR döner (kaydedilmez). manifest:true derlemeye gidecek manifesti de ekler (manifest-preview; sayaçlar SIRADAKİ numara); üretilemiyorsa manifestError alan adıyla döner. detail ayrıntı bölümleri ekler: 'assets' görselleri id'li liste olarak döner (mobile_asset_update / mobile_asset_delete id ister), 'signing' uygulamanın kimlik bilgilerini (signing.app: iOS profili, Android keystore, harita anahtarı, demo hesabı) ve organizasyonun Apple/Google hesaplarını + sertifikalarını (signing.accounts), 'runners' çevrimiçi runner'ları, 'audit' son 20 gizli bilgi olayını (subject: hangi tür, kim, ne zaman, hangi iş), 'permissions' izin görünümünü (permissions.permissions[]: uygulamanın isteyebileceği her izin için mode, text/defaultText, usage — izni kullanan sayfa·bileşen ya da akış adımı (setter ile yazılan tetikleyici prop dahil), ilk 20 yer + usageCount, çağrının environment'ından; usageReadable false ise o ortam okunamamıştır ve boş usage 'kullanılmıyor' demek değildir —, build — mağaza derlemesi taşıyor mu ve neden; HER ZAMAN production'dan: build.usageCount production'daki kullanım, build.staticUsageCount onun yazılı (ifadesiz) kısmı —, devices — aktif cihazların izin durumu sayıları; missingFromStoreBuilds — o izni taşımayan son mağaza derlemeleri; shell.carriesPermissions false ise kipler ve kabuğun henüz taşımadığı izinler bir sonraki kabuk sürümüyle etkili olur; shell.carriesLaunchPermissions false ise açılışta sorulan izinler (askOnLaunch) o kabukta sorulmaz, shell.carriesAppLock false ise açık uygulama kilidi o kabukta yoktur — permissionWarnings söyler), 'status' mağaza durumunu (GET store-status: apple { state, appStoreState, versionString, releaseType, phased* }, play.tracks[] { track, status, versionCodes, rolloutPercent }, actions[] { platform, target, track, irreversible, consequence } — Studio › Yayınla'daki yayın düğmeleri; irreversible:true geri alınamaz, eylemi MCP başlatmaz, kullanıcıya consequence'ı söyleyip Studio'ya yönlendirin), 'changes' son production dağıtımından beri değişenleri iki kovada (GET changes-since-production: live — dağıtımla telefonlara gider, mağaza gerekmez; store — izin/kimlik/görsel gibi yeni mağaza sürümü isteyenler). shell HER ÇAĞRIDA döner (GET mobile-store/shell, tarama yapmaz): yayındaki kabuk paketinin sürümü ve yetenek tablosu (shell.capabilities: ad → { carried, since }; ör. backStep, keyboardInsets, flowDrain — permissions.shell.capabilities aynı tablonun backend'deki ham dizi biçimidir) — bir özelliğin telefonda çalışıp çalışmadığını ve bir geçici çözümün kaldırılıp kaldırılamayacağını buradan okuyun (platform_guide('mobile-publish') §Kabuk sürümleri); okunamazsa shellError döner, çağrı düşmez. Gizli DEĞER hiçbir bölümde yoktur — yalnız tür, maskeli id ve meta (ekip, son kullanma); bir bölüm okunamazsa (uç hatası) <bölüm>Error döner, çağrı düşmez. Gizli bilgiyi EKLEMEK/DEĞİŞTİRMEK MCP'den yapılmaz — Studio › Yayın › Hesaplar.",
392
392
  inputSchema: {
393
393
  applicationId,
394
394
  manifest: z.boolean().optional().describe('true: derleme manifestinin önizlemesini de ekler'),
@@ -402,6 +402,17 @@ export function registerMobileStoreTools(server, platform, storeArt = {}) {
402
402
  const store = storeSummary(unwrap(await platform.org('GET', `/mobile-store/${appId}`)), sections.has('assets'));
403
403
  const details = await Promise.all(DETAIL_SECTIONS.filter(section => section !== 'assets' && sections.has(section)).map(section => readDetail(platform, appId, section, { ...(project ? { project } : {}), ...(environment ? { environment } : {}) })));
404
404
  const result = Object.assign(store, ...details);
405
+ // g-05: kabuk tablosu yapılandırmadan okunur (GET mobile-store/shell, tarama yok); düşmesi görünümü düşürmez.
406
+ try {
407
+ const shell = shellCapabilityView(unwrap(await platform.org('GET', '/mobile-store/shell')));
408
+ if (shell)
409
+ result.shell = shell;
410
+ }
411
+ catch (error) {
412
+ if (!(error instanceof PlatformApiError))
413
+ throw error;
414
+ result.shellError = error.message;
415
+ }
405
416
  if (!manifest)
406
417
  return result;
407
418
  try {
@@ -1,5 +1,5 @@
1
1
  import { guid, projectParam, environmentParam, pagingParams, asUserIdParam, readOnly, writes, updates, destructive, microOps, isBranchName, branchSlug } from './common.js';
2
- import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEventOps, stripGatewayEventFields, staleSetterWarnings, lostSetterTargets, fillComponentNames, componentNameNote, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, codePropTemplateErrors, addComponentIdErrors, missingBlocksNodes, retiredTypeNodes, assertNoRetiredNodes, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, spacingLayerWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, flexIndexOrder, flexIndexOrderNote, withNormalization, parseVariableEntries, PAGE_CONTAINER_ELEMENT_ID, screenEventsNeedPlatform } from '../knowledge/normalize.js';
2
+ import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEventOps, flowIdEventOps, stripGatewayEventFields, staleSetterWarnings, lostSetterTargets, fillComponentNames, componentNameNote, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, codePropTemplateErrors, addComponentIdErrors, missingBlocksNodes, retiredTypeNodes, assertNoRetiredNodes, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, spacingLayerWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, flexIndexOrder, flexIndexOrderNote, withNormalization, parseVariableEntries, PAGE_CONTAINER_ELEMENT_ID, screenEventsNeedPlatform } from '../knowledge/normalize.js';
3
3
  import { functionSignature } from '../knowledge/functionGuards.js';
4
4
  import { attachFindings } from '../knowledge/policy.js';
5
5
  import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode, flowPath, countSchemaNodes, missingElementIds, readFullSchema } from './screens.js';
@@ -7,7 +7,7 @@ import { fetchComponentEventNames } from './components.js';
7
7
  import { assertScreenWriteGuards, screenCatalogNotes, assertScreenCatalog, unknownPropOps } from './screenWrite.js';
8
8
  import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, applySkeletonOps, reshapesSkeleton, planEventRemovals, callerSlots, planEmulatedOps, recordsWithEventRemovals, serverOpsInOrder, rejectedOpsMessage } from './schemaOps.js';
9
9
  import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
10
- import { emptyWriteDiff, schemaOpDrops, unwrapProps, withWriteDiff, writeDiff } from '../platform/writeDiff.js';
10
+ import { emptyWriteDiff, pageWriteDrops, schemaOpDrops, unwrapProps, withWriteDiff, writeDiff } from '../platform/writeDiff.js';
11
11
  import { PlatformApiError } from '../platform/errors.js';
12
12
  import { existingMatch, ifExistsParam, listAll, withOutcome } from '../platform/ifExists.js';
13
13
  import { handle, toolResult, toolError } from '../platform/responses.js';
@@ -325,7 +325,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
325
325
  dx: z.number().optional().describe("Yalnız 'swipe': jestin yatay kayması (CSS pikseli; sola kaydırma eksi). to ile birlikte verilmez"),
326
326
  dy: z.number().optional().describe("Yalnız 'swipe': jestin dikey kayması (CSS pikseli; yukarı eksi). to ile birlikte verilmez"),
327
327
  blur: z.boolean().optional().describe("Yalnız 'set': false verilirse odak alanda KALIR (telefonda klavye açıkken yazılan hâl) — input+change atılır, blur/focusout atılmaz; odak kaybına bağlı akış ('changed') bu adımda KOŞMAZ. Varsayılan true: gerçek kullanıcı gibi yazıp alandan çıkar")
328
- })).max(20).optional().describe("Kare ALINMADAN ÖNCE sayfada koşturulacak etkileşim adımları: [{action:'set', target:'flt_month', value:'2022-10'}, {action:'click', target:'btn_sign_in'}, {action:'tab', target:'tabs_settings', value:'iot'}, {action:'select', target:'dd_city', value:'34'}, {action:'submit', target:'frm_note'}, {action:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}]. 'tab' SEKME SEÇER: sekme grubunun köküne 'click' göndermek DOM sırasındaki İLK sekmeye gider ve ikinci sekme hiç açılamıyordu — tablist köküne click artık reddedilir ve bu fiile yönlendirir. Eşleme önce '<elementId>__tab-<sekme anahtarı>' kimliğiyle, o yoksa görünen metinle yapılır; actionsReport.matchedBy hangisi olduğunu, actionsReport.activeTab adımdan SONRA hangi sekmenin açık olduğunu söyler. Etkin olmayan sekmenin içeriği DOM'da YOKTUR: o sekmedeki bir düğüme yazmadan önce 'tab' adımı gerekir. 'select' açılır kutudan seçer — kutu native alan değildir, değeri yazmak hiçbir şey yapmaz, seçeneğe tıklanması gerekir; liste 3 sn'ye kadar YOKLANIR (sorgudan beslenen kutuda seçenekler geç basılır) ve actionsReport.optionsSeen + waitedMs neyin görüldüğünü söyler. Autocomplete'te 'set' metni alana yazar, 'select' metni yazıp öneri satırını GÖRÜNEN METİNLE seçer (value ile değil; eşleşme yoksa yazılan metin alanda kalır ve note bunu söyler). 'submit' formu gönder butonu OLMADAN gönderir (form.requestSubmit); actionsReport.submitVia hangi yolun kullanıldığını yazar. 'drag' HTML5 sürükle-bırak olaylarını (dragstart→dragover→drop→dragend) hedefin merkezinde atar; kanban cardMove ve takvim eventMove bununla tetiklenir. 'swipe' DOKUNMATİK jesttir (mobil): hedefin merkezine basar, 12 adımda 16 ms arayla taşır ve bırakır (pointer + mouse olayları; sıfır kayma reddedilir — dokunuş 'click'tir) — tutamaçtan sıralama {action:'swipe', target:'lst__row-2__handle', to:'lst__row-0'}, satır kaydırma eylemi {action:'swipe', target:'lst__row-1', dx:-160, dy:0}; 'drag' web'in HTML5 sürüklemesi, 'swipe' dokunmatik jest. 'network' bağlamın AĞINI keser ya da açar ({action:'network', value:'offline'} → ağsız yaz/kaydet → {action:'network', value:'online'}): sayfanın tamamı etkilenir, hedef almaz; actionsReport.online sayfanın gördüğü navigator.onLine'dır, ağ açılınca koşan eşitleme akışları flowRuns'a yazılır. Çağrının sonunda ağ yeniden açılır; ağsız davranışı iki çağrı arasında sürdürmek için session ile depo taşınır, ağ taşınmaz. 'set' + blur:false odağı alanda bırakır (telefonda klavye açıkken). 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez); ardından kök düğümün runtime prop'larını yeniden okur: actionsReport.propWritten değerin prop'a ULAŞIP ulaşmadığıdır ve false ise applied da false döner — DOM'a yazılmış ama bileşenin duymadığı değer 'uygulandı' sayılmaz (G-14). Her adımdan sonra yerleşme beklenir ve yanıttaki actionsReport hedefin bulunup bulunmadığını ve adımdan sonra KAÇ AĞ İSTEĞİ çıktığını söyler — 'bağ kuruldu' ile 'çalışıyor' arasındaki farkı ancak bu ayırır (G-24)."),
328
+ })).max(20).optional().describe("Kare ALINMADAN ÖNCE sayfada koşturulacak etkileşim adımları: [{action:'set', target:'flt_month', value:'2022-10'}, {action:'click', target:'btn_sign_in'}, {action:'tab', target:'tabs_settings', value:'iot'}, {action:'select', target:'dd_city', value:'34'}, {action:'submit', target:'frm_note'}, {action:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}]. 'click' hedefi seçer (düğümün kendisi basılabilirse o → <elementId>__press → içteki ilk düğme) ve hedefin görünen bir noktasına GERÇEK fare olayıyla basar, telefondaki parmak gibi: önce ortaya, ortada hedefin içindeki başka basılabilir öğe (kabın düğmesi, çipin kaldır'ı) varsa kutunun başka bir noktasına; görünmeyen hedef önce görünüme kaydırılır. Hiçbir noktası hedefin değilse eylem UYGULANMAZ: actionsReport.coveredBy örten öğenin elementId'sidir (şema düğümü değilse rolü ve metni) — telefonda da dokunuşu o alır. 'tab' SEKME SEÇER: sekme grubunun köküne 'click' göndermek DOM sırasındaki İLK sekmeye gider ve ikinci sekme hiç açılamıyordu — tablist köküne click artık reddedilir ve bu fiile yönlendirir. Eşleme önce '<elementId>__tab-<sekme anahtarı>' kimliğiyle, o yoksa görünen metinle yapılır; actionsReport.matchedBy hangisi olduğunu, actionsReport.activeTab adımdan SONRA hangi sekmenin açık olduğunu söyler. Etkin olmayan sekmenin içeriği DOM'da YOKTUR: o sekmedeki bir düğüme yazmadan önce 'tab' adımı gerekir. 'select' açılır kutudan seçer — kutu native alan değildir, değeri yazmak hiçbir şey yapmaz, seçeneğe tıklanması gerekir; liste 3 sn'ye kadar YOKLANIR (sorgudan beslenen kutuda seçenekler geç basılır) ve actionsReport.optionsSeen + waitedMs neyin görüldüğünü söyler. Autocomplete'te 'set' metni alana yazar, 'select' metni yazıp öneri satırını GÖRÜNEN METİNLE seçer (value ile değil; eşleşme yoksa yazılan metin alanda kalır ve note bunu söyler). 'submit' formu gönder butonu OLMADAN gönderir (form.requestSubmit); actionsReport.submitVia hangi yolun kullanıldığını yazar. 'drag' HTML5 sürükle-bırak olaylarını (dragstart→dragover→drop→dragend) hedefin merkezinde atar; kanban cardMove ve takvim eventMove bununla tetiklenir. 'swipe' DOKUNMATİK jesttir (mobil): hedefin merkezine basar, 12 adımda 16 ms arayla taşır ve bırakır (pointer + mouse olayları; sıfır kayma reddedilir — dokunuş 'click'tir) — tutamaçtan sıralama {action:'swipe', target:'lst__row-2__handle', to:'lst__row-0'}, satır kaydırma eylemi {action:'swipe', target:'lst__row-1', dx:-160, dy:0}; 'drag' web'in HTML5 sürüklemesi, 'swipe' dokunmatik jest. 'network' bağlamın AĞINI keser ya da açar ({action:'network', value:'offline'} → ağsız yaz/kaydet → {action:'network', value:'online'}): sayfanın tamamı etkilenir, hedef almaz; actionsReport.online sayfanın gördüğü navigator.onLine'dır, ağ açılınca koşan eşitleme akışları flowRuns'a yazılır. Çağrının sonunda ağ yeniden açılır; ağsız davranışı iki çağrı arasında sürdürmek için session ile depo taşınır, ağ taşınmaz. 'set' + blur:false odağı alanda bırakır (telefonda klavye açıkken). 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez); ardından kök düğümün runtime prop'larını yeniden okur: actionsReport.propWritten değerin prop'a ULAŞIP ulaşmadığıdır ve false ise applied da false döner — DOM'a yazılmış ama bileşenin duymadığı değer 'uygulandı' sayılmaz (G-14). Her adımdan sonra yerleşme beklenir ve yanıttaki actionsReport hedefin bulunup bulunmadığını ve adımdan sonra KAÇ AĞ İSTEĞİ çıktığını söyler — 'bağ kuruldu' ile 'çalışıyor' arasındaki farkı ancak bu ayırır (G-24)."),
329
329
  session: z.string().regex(/^[a-z0-9-]{1,40}$/).optional().describe("Çağrılar arası tarayıcı oturumu (kısa ad, [a-z0-9-]{1,40}): verilirse çekim sonunda çerezler, localStorage ve IndexedDB gateway BELLEĞİNDE saklanır ve aynı adla gelen sonraki çekim o depoyla açılır — kalıcı state.app ve çevrimdışı servis kuyruğu ikinci çağrıda okunur. Yanıttaki sessionRestored önceki depo yüklendi mi söyler (ilk çağrıda false). Kimlik ve adres anahtara dahildir: aynı adla farklı kullanıcı (jetonun sub'ı; yenilenen jeton aynı oturumdur), asUserId, cookies, uygulama ya da ortam ayrı oturumdur. Depo okunamazsa oturum silinir ve yanıt sessionSaved: false der. Ömür 30 dk, en çok 8 oturum (en eskisi düşer); gateway yeniden başlayınca silinir. sessionStorage (güvenli depo) taşınmaz. Ağ durumu taşınmaz: her çekim ağ AÇIK başlar. Verilmezse her çekim boş depoyla başlar (bugünkü yalıtım)."),
330
330
  language: z.string().min(1).optional().describe("Kareyi BU DİLDE çeker: adrese '?lang=<kod>' eklenir ve runtime dili oradan seçer (öncelik: ?lang → tarayıcıda kayıtlı seçim → tarayıcı dilleri → projenin varsayılanı). Seçim hem çeviriye bağlı prop'ları hem çok dilli alanların satır değerlerini kapsar, yani ikinci dilin karesi için sayfaya dil değiştirici KOYMAK GEREKMEZ — referans tasarımı bozulmadan ikinci dil kanıtlanır. ISO 639-1 iki harfli kod ('tr', 'en'); bölgeli kod ('tr-TR') indirgenir ve indirgeme yanıtta bildirilir. Kod projenin languages listesinde yoksa çağrı REDDEDİLİR: runtime tanımadığı kodu yok sayıp kareyi varsayılan dilde çizerdi ve 'çevrildi' sanılan yanlış bir kare raporlanırdı. MOBİL uygulamada da geçerlidir (/preview aynı ?lang'i okur); cihazın kendisinde ?lang yoktur (platform_guide('mobile') §Çok dil)."),
331
331
  colorScheme: z.enum(['light', 'dark']).optional().describe("Kareyi açık ya da koyu temada çeker. Ziyaretçi tema tercihi çerezde değil localStorage'da yaşar ve tema store'u onu sayfa açılırken bir kez okur; gateway tercihi sayfa script'lerinden ÖNCE eker, bu yüzden sayfada 'theme-toggle' komponenti OLMASA DA koyu tema karesi alınabilir. Verilmezse projenin kendi modu çizilir."),
@@ -720,7 +720,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
720
720
  });
721
721
  server.registerTool('page_schema_get', {
722
722
  title: 'Sayfa şemasını oku',
723
- description: "Sayfanın komponent şemasını okur. Yanıttaki version değerini page_schema_patch'e baseVersion olarak verin: sayfa o okumadan beri değiştiyse yama 409 ile reddedilir ve hiçbir op uygulanmaz. 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 — her parça items[] ({ parentElementId, index, node }) taşır, kalem parentElementId düğümünün children[index]'inin yerine geçer (parentElementId yoksa kök dizinin [index]'i), alt ağacı sığmayan düğüm kabuk gelir (children: [{ elementId, deferred: true }]) ve sonraki parçalar onu doldurur; nextCursor bitene kadar sürün. allowTruncated:true kırpılmış gövdeyi açıkça verir (truncated: true; hangi düğümlerin düştüğü söylenmez) — o gövdeyi page_update'e geri yazmayın (PAGE-043).",
723
+ description: "Sayfanın komponent şemasını okur. Yanıttaki version değerini page_schema_patch'e baseVersion olarak verin: sayfa o okumadan beri değiştiyse yama 409 ile reddedilir ve hiçbir op uygulanmaz. YANIT ŞEKLİ kipe göre değişir ve schemaShape alanı bunu söyler: summary ve full'da schema DİZİDİR (schemaShape: 'list'), subtree'de schema TEK DÜĞÜMDÜR (schemaShape: 'node', dizi değil). 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 — her parça items[] ({ parentElementId, index, node }) taşır, kalem parentElementId düğümünün children[index]'inin yerine geçer (parentElementId yoksa kök dizinin [index]'i), alt ağacı sığmayan düğüm kabuk gelir (children: [{ elementId, deferred: true }]) ve sonraki parçalar onu doldurur; nextCursor bitene kadar sürün. allowTruncated:true kırpılmış gövdeyi açıkça verir (truncated: true; hangi düğümlerin düştüğü söylenmez) — o gövdeyi page_update'e geri yazmayın (PAGE-043).",
724
724
  inputSchema: {
725
725
  project: projectParam,
726
726
  environment: environmentParam,
@@ -747,7 +747,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
747
747
  field: 'schema',
748
748
  nodes: page.schema ?? [],
749
749
  versionKey: String(version ?? ''),
750
- base: { version, updatedAt, ...typeField },
750
+ base: { version, updatedAt, ...typeField, schemaShape: 'list' },
751
751
  cursor,
752
752
  allowTruncated,
753
753
  maxResponseBytes: config.maxResponseBytes,
@@ -760,9 +760,9 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
760
760
  const subtree = findNode(page.schema, componentId);
761
761
  if (!subtree)
762
762
  throw new PlatformApiError(`Sayfada '${componentId}' id'li bir komponent yok; page_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
763
- return { version, updatedAt, ...typeField, nodeCount: countSchemaNodes([subtree]), schema: subtree };
763
+ return { version, updatedAt, ...typeField, schemaShape: 'node', nodeCount: countSchemaNodes([subtree]), schema: subtree };
764
764
  }
765
- return { version, updatedAt, ...typeField, schema: (page.schema ?? []).map(node => summarize(node)) };
765
+ return { version, updatedAt, ...typeField, schemaShape: 'list', schema: (page.schema ?? []).map(node => summarize(node)) };
766
766
  }));
767
767
  server.registerTool('page_state', {
768
768
  title: 'Sayfa durumunu getir',
@@ -850,6 +850,8 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
850
850
  for (const issue of connectDataOpIssues(entry, connectedProps))
851
851
  reject(issue.code, entry, [issue.message]);
852
852
  }
853
+ for (const entry of opRecords)
854
+ reject('PAGE-052', entry, flowIdEventOps([entry]));
853
855
  if (opRecords.some(entry => entry.op === 'set_event' || entry.op === 'add_event')) {
854
856
  const typesByElementId = new Map();
855
857
  try {
@@ -1130,7 +1132,11 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
1130
1132
  // G-23 micro-op ayağı: yama 200 döner ve `appliedPatch` op'un ŞEMAYA yazıldığını söyler, ama
1131
1133
  // alanın gerçekten KAYDEDİLDİĞİNİ söylemez. Yamadan sonra şemayı bir kez okuyup op'un yazmak
1132
1134
  // istediğiyle kayıtlı olanı karşılaştırıyoruz. Okuma başarısız olursa yama yine geçerlidir.
1133
- const contentOps = sentOps.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
1135
+ // Olay op'ları platforma giden SIRALI listeden alınır: `remove_event` yalnız orada durur ve önceki bağı geçersiz kılar.
1136
+ const contentOps = [
1137
+ ...sentOps.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop'),
1138
+ ...serverOps.filter(entry => ['set_event', 'add_event', 'remove_event'].includes(String(entry.op)))
1139
+ ];
1134
1140
  let opDrops = emptyWriteDiff();
1135
1141
  if (contentOps.length > 0) {
1136
1142
  try {
@@ -1244,6 +1250,10 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
1244
1250
  const changed = Object.keys(overrides).filter(key => JSON.stringify(before[key]) !== JSON.stringify(overrides[key]));
1245
1251
  const written = await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
1246
1252
  rememberVersion(project, environment, pageId, written?.data?.version);
1253
+ const sentCounts = { events: body.events, schemaNodes: countSchemaNodes(body.schema) };
1254
+ const savedCounts = Array.isArray(written?.data?.events) && Array.isArray(written?.data?.schema)
1255
+ ? { events: written.data.events, schemaNodes: countSchemaNodes(written.data.schema) }
1256
+ : undefined;
1247
1257
  const updateWarnings = schema === undefined
1248
1258
  ? []
1249
1259
  : [...gaps, ...stringStyleWarning(stringStyles), ...(resolved.profile.layout.modes.has('grid') ? inertFlexPropWarning(inertFlexPropNodes(container.schema)) : []), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'), ...spacingLayerWarnings(container.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)'), ...profileWarnings,
@@ -1252,12 +1262,12 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
1252
1262
  id: pageId,
1253
1263
  outcome: 'updated',
1254
1264
  changed,
1255
- preserved: { schemaNodes: countSchemaNodes(body.schema), events: body.events.length },
1265
+ preserved: { schemaNodes: (savedCounts ?? sentCounts).schemaNodes, events: (savedCounts ?? sentCounts).events.length },
1256
1266
  ...applicationTypeField(resolved)
1257
1267
  };
1258
1268
  if (updateWarnings.length > 0)
1259
1269
  summary.layoutWarnings = updateWarnings;
1260
- return attachFindings(withNormalization(summary, schema === undefined ? [] : [...container.fixes, ...skeletonNotes, ...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved), ...mobileSpacingNote(spacing)]), textFindings);
1270
+ return attachFindings(withWriteDiff(withNormalization(summary, schema === undefined ? [] : [...container.fixes, ...skeletonNotes, ...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved), ...mobileSpacingNote(spacing)]), pageWriteDrops(sentCounts, savedCounts)), textFindings);
1261
1271
  }
1262
1272
  // PAGE-051 yalnız gerçekten OLUŞTURAN dalda: `ifExists: 'update'` var olan sayfada page_update gibi
1263
1273
  // davranır ve kayıtlı emekli düğüm yazılabilir kalmalıdır.
@@ -1391,17 +1401,22 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
1391
1401
  throw await versionConflict(error, project, environment, applicationId, pageId, String(body.baseVersion ?? ''), "page_schema_get ile yeniden okuyun ve şema değişikliğini page_schema_patch ile uygulayın — yalnız dokunduğunuz yer yazılır, aradaki değişiklik korunur. Üstbilgi (ad, slug, layout, roller) için okuduktan sonra page_update'i yeniden çağırın; aradaki değişikliği bilerek ezmek için force:true.");
1392
1402
  });
1393
1403
  rememberVersion(project, environment, pageId, written?.data?.version);
1404
+ // PAGE-053: sayılar PUT yanıtındaki KAYITLI sayfadan; gönderilen gövde yalnız yanıt kaydı taşımazsa kullanılır.
1405
+ const sentCounts = { events: body.events, schemaNodes: countSchemaNodes(body.schema) };
1406
+ const savedCounts = Array.isArray(written?.data?.events) && Array.isArray(written?.data?.schema)
1407
+ ? { events: written.data.events, schemaNodes: countSchemaNodes(written.data.schema) }
1408
+ : undefined;
1394
1409
  const summary = {
1395
1410
  id: pageId,
1396
1411
  changed: Object.keys(overrides),
1397
- preserved: { schemaNodes: countSchemaNodes(body.schema), events: body.events.length }
1412
+ preserved: { schemaNodes: (savedCounts ?? sentCounts).schemaNodes, events: (savedCounts ?? sentCounts).events.length }
1398
1413
  };
1399
1414
  if (force === true)
1400
1415
  summary.forced = true;
1401
1416
  if (gaps.length > 0 || skeletonWarnings.length > 0)
1402
1417
  summary.layoutWarnings = [...gaps, ...skeletonWarnings];
1403
1418
  const findings = [...await legacyRoleListFindings(platform, project, environment, overrides, 'page'), ...textFindings];
1404
- return attachFindings(withNormalization(summary, [...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), findings);
1419
+ return attachFindings(withWriteDiff(withNormalization(summary, [...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), pageWriteDrops(sentCounts, savedCounts)), findings);
1405
1420
  }));
1406
1421
  server.registerTool('page_clone', {
1407
1422
  title: 'Sayfa klonla',