@appaflytech/zeyrek-ai-mcp 0.0.37 → 0.0.40

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 (37) hide show
  1. package/dist/knowledge/componentCatalog.js +389 -149
  2. package/dist/knowledge/componentEvents.js +16 -8
  3. package/dist/knowledge/connectDataBind.js +10 -2
  4. package/dist/knowledge/expressionPrelude.js +1 -0
  5. package/dist/knowledge/mobileComponentCatalog.js +16 -4
  6. package/dist/knowledge/mobileComponentEvents.js +21 -1
  7. package/dist/knowledge/mobileStore.js +75 -0
  8. package/dist/knowledge/normalize.js +22 -5
  9. package/dist/knowledge/platform/mobile.js +1 -1
  10. package/dist/knowledge/platform/mobileRules.js +1 -1
  11. package/dist/knowledge/platform/web.js +1 -1
  12. package/dist/knowledge/rules.js +79 -2
  13. package/dist/knowledge/styleResolve.js +27 -7
  14. package/dist/knowledge/styleResolveNative.js +4 -2
  15. package/dist/knowledge/themeResolve.js +1 -1
  16. package/dist/knowledge/themeSchema.js +2 -1
  17. package/dist/observability/trace.js +7 -2
  18. package/dist/platform/secrets.js +50 -2
  19. package/dist/resources/guides/component.md +1 -1
  20. package/dist/resources/guides/merge.md +5 -4
  21. package/dist/resources/guides/microservice.md +1 -1
  22. package/dist/resources/guides/middleware.md +1 -1
  23. package/dist/resources/guides/mobile-publish.md +24 -0
  24. package/dist/resources/guides/mobile.md +22 -6
  25. package/dist/resources/guides/page.md +14 -7
  26. package/dist/resources/guides/theme.md +1 -0
  27. package/dist/resources/schema-fingerprint.json +3 -3
  28. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +2 -1
  29. package/dist/resources/schemas/OrganizationProjectDto.json +8 -0
  30. package/dist/resources/schemas/UpsertMobileStoreSettingsCommand.json +59 -0
  31. package/dist/tools/applications.js +6 -3
  32. package/dist/tools/common.js +9 -5
  33. package/dist/tools/environments.js +1 -1
  34. package/dist/tools/flows.js +5 -1
  35. package/dist/tools/mobileStore.js +21 -5
  36. package/dist/tools/pages.js +3 -3
  37. package/package.json +1 -1
@@ -108,18 +108,18 @@ export const componentEventContracts = {
108
108
  mount: NO_DATA,
109
109
  change: {
110
110
  payload: { value: 'FilterGroup (iç içe and/or kural ağacı)', jexl: 'string (aynı ağacın hazır jexl ifadesi)' },
111
- reads: "setter yalnız `value`yi self’e yazar; `jexl` YALNIZ event.data’dadır (params) — sorguya bağlayacağın alan odur"
111
+ reads: "setter yalnız `value`yi self’e yazar; `jexl` YALNIZ event.data’dadır (params) — sorguya bağlayacağın alan odur. `value` (ağaç) de event.data’da gelir — State’e geri yazmak için event.data.value’yu kullan. Ağaç değişmediyse (etkin birleştiriciye yeniden basmak) olay yayılmaz"
112
112
  }
113
113
  },
114
114
  comments: {
115
115
  mount: NO_DATA,
116
116
  submit: {
117
117
  payload: { text: 'string', parentId: "string (yanıtsa üst yorumun id’si, kök yorumda '')", mentions: 'string[] (metinden çözülmüş @kişi listesi)', author: 'string (currentUser)', process: "{ scope: 'task' | 'instance', targetId, visibility, body (@Ad → @<id>), mentionUserIds } | null (serbest yorumda null)" },
118
- reads: 'Komponent yorumu ANINDA iyimser olarak listeye ekler (bekleyen katman); kalıcılık akışın işidir — rows tazelenmezse bekleyen satır bir sonraki içerik değişiminde kaybolur'
118
+ reads: 'Yük YALNIZ event.data’dadır (self.text/self.mentions boş gelir): {{ event.data.text }}. Komponent yorumu ANINDA iyimser olarak listeye ekler (bekleyen katman); kalıcılık akışın işidir — bekleyen satır, rows’a aynı üst yorum + metin (+ yazar) ile YENİ bir satır gelince yerini ona bırakır; akış kaydetmezse ya da metni değiştirirse "gönderiliyor…" kalır'
119
119
  },
120
- like: { payload: { id: 'string', liked: 'boolean (yeni durum)', author: 'string (currentUser — işlemi yapan)' }, reads: 'beğeni de iyimserdir; sayaç rows tazelenene kadar yerel. isDisabled/isReadonly/isViewmode açıkken yayılmaz' },
121
- edit: { payload: { id: 'string', text: 'string (yeni metin)', author: 'string (currentUser — işlemi yapan)' }, reads: 'düzenleme de iyimserdir; yeni metin "düzenlendi" işaretiyle rows tazelenene kadar yerel. Metin değişmediyse yayılmaz' },
122
- delete: { payload: { id: 'string', author: 'string (currentUser — işlemi yapan)' }, reads: 'satır (kök yorumsa yanıtlarıyla birlikte) listeden hemen gizlenir; uca yazmak akışın işidir' }
120
+ like: { payload: { id: 'string', liked: 'boolean (yeni durum)', author: 'string (currentUser — işlemi yapan)' }, reads: 'Yük yalnız event.data’dadır. Beğeni de iyimserdir; sayaç rows tazelenene kadar yerel. isDisabled/isReadonly/isViewmode açıkken yayılmaz' },
121
+ edit: { payload: { id: 'string', text: 'string (yeni metin)', author: 'string (currentUser — işlemi yapan)' }, reads: 'Yük yalnız event.data’dadır. Düzenleme de iyimserdir; yeni metin "düzenlendi" işaretiyle rows tazelenene kadar yerel. Metin değişmediyse yayılmaz' },
122
+ delete: { payload: { id: 'string', author: 'string (currentUser — işlemi yapan)' }, reads: 'Yük yalnız event.data’dadır. Satır (kök yorumsa yanıtlarıyla birlikte) listeden hemen gizlenir; uca yazmak akışın işidir' }
123
123
  },
124
124
  'auth-layout': { mount: NO_DATA, primaryClick: NO_DATA, secondaryClick: NO_DATA },
125
125
  layout: { mount: NO_DATA, logout: NO_DATA },
@@ -354,7 +354,7 @@ export const componentEventContracts = {
354
354
  'audit-basic': { mount: NO_DATA },
355
355
  'audit-history': {
356
356
  mount: NO_DATA,
357
- showMore: { payload: {}, reads: 'yalnız histories visibleLogCount’u aşınca çıkan "Daha fazla göster" düğmesine basılınca; görünür sınır visibleLogCount kadar büyür, kalan kalmayınca düğme kaybolur. Bileşen veri çekmez; backend kayıt başına en fazla 5 işlem verdiği için meta.history bağlıyken düğme ancak visibleLogCount < 5 iken çıkar' }
357
+ showMore: { payload: {}, reads: 'yalnız histories visibleLogCount’u aşınca çıkan "Daha fazla göster" düğmesine basılınca; görünür sınır visibleLogCount kadar büyür, kalan kalmayınca düğme kaybolur; en yeni (ilk) girdi değişince sınır başa döner. Bileşen veri çekmez; backend kayıt başına en fazla 5 işlem verdiği için meta.history bağlıyken düğme ancak visibleLogCount < 5 iken çıkar' }
358
358
  },
359
359
  'form-validation-errors': { mount: NO_DATA },
360
360
  'page-header': {
@@ -381,7 +381,7 @@ export const componentEventContracts = {
381
381
  },
382
382
  click: {
383
383
  payload: {},
384
- reads: 'Yalnız akışa bağlıysa sarmalayıcı tıklanabilir olur; controls açıkken oynat/duraklat düğmesine basmak da bu olayı tetikler (tıklama sarmalayıcıya kabarır). isLoading:true iken HİÇ çalışmaz — o dalda oynatıcı yerine iskelet çizilir ve sarmalayıcı tıklanabilir değildir'
384
+ reads: "Yalnız akışa bağlıysa çalışır (sarmalayıcı <div>; controls:false iken role=button + Enter/Boşluk da tetikler); controls açıkken videonun YÜZEYİNE tıklamak hem oynatır hem bu olayı tetikler, kontrol çubuğundaki yerel oynat/duraklat düğmesi ise videoyu oynatır AMA bu olayı TETİKLEMEZ (ölçüldü 2026-10-05). isLoading:true iken HİÇ çalışmaz — o dalda oynatıcı yerine iskelet çizilir ve sarmalayıcı tıklanabilir değildir"
385
385
  },
386
386
  play: {
387
387
  payload: { isPlaying: 'boolean (true)' },
@@ -394,6 +394,14 @@ export const componentEventContracts = {
394
394
  ended: {
395
395
  payload: { isPlaying: 'boolean (false)' },
396
396
  reads: 'loop açıkken HİÇ yayılmaz (döngü videosu bitmez). event.data: { currentTime, duration } — bitişte currentTime pratikte duration’a eşittir. Bitişte pause’dan SONRA gelir (spesifikasyon sırası), yani ended akışı pause akışının ardından koşar'
397
+ },
398
+ load: {
399
+ payload: {},
400
+ reads: "Kaynağın İLK KARESİ hazır olunca (loadeddata) yayılır; kaynak her değiştiğinde yeniden. Değer yazmaz (setter yok). event.data: { duration: number (saniye; sonsuz ya da bilinmiyorsa 0), width: number, height: number (kaynağın kendi piksel boyu) }. Yalnız oynatıcı çizilirken yayılır: isLoading:true iken <video> yoktur, load gelmez (iskeleti kaynağı çözen sorgudan kapat). Boş src'de ya da error'da yayılmaz"
401
+ },
402
+ error: {
403
+ payload: {},
404
+ reads: "Kaynak yüklenemediğinde yayılır (404, video olmayan dosya, desteklenmeyen biçim, ağ kopması). Değer yazmaz, ama isPlaying true ise false'a düşer. event.data: { code: number (MediaError.code: 1 iptal · 2 ağ · 3 çözümleme · 4 kaynak yok/desteklenmiyor), message: string (tarayıcıya bağlı, çoğu kez boş) }. Bileşen kendi hata görünümü çizmez: siyah kutu kalır, kullanıcıya bildirmek akışın işidir. Boş src'de yayılmaz"
397
405
  }
398
406
  },
399
407
  // — 2026-08-25 eşleme turu: ng-apps-ui'da olay YAYAN ama burada kaydı olmayan komponentler.
@@ -672,7 +680,7 @@ export const componentEventContracts = {
672
680
  fileName: 'string (okunan dosyanın adı; henüz dosya yoksa "")',
673
681
  rowCount: 'number (dosyadaki HAM veri satırı sayısı — doğrulamadan önce)'
674
682
  },
675
- reads: "adım her değiştiğinde yayılır; currentStep ve fileName düğüme de yazılır (self.currentStep), rowCount yalnız event.data'dadır"
683
+ reads: "adım her değiştiğinde yayılır; currentStep ve fileName düğüme de yazılır (self.currentStep), rowCount yalnız event.data'dadır. resetAfterComplete açıkken complete'ten hemen sonra { currentStep: 0, fileName: tamamlanan dosya, rowCount: 0 } ile bir kez daha yayılır"
676
684
  },
677
685
  complete: {
678
686
  payload: {
@@ -375,7 +375,11 @@ export function buildDefinition(input, describe, connectable, connectablePaths =
375
375
  for (const slug of (keptSlugs(input.edit?.editable) ?? []).filter(item => describe.fields.find(field => field.slug === item)?.type === 'file')) {
376
376
  block('CD-018', `'${slug}' dosya alanı: edit.editable'a konamaz — bağlı bileşen dosyayı satır içinde yazmaz, platform kaydı 422 connect-data-editable-file ile reddeder. '${slug}'i editable'dan çıkarın; dosyayı file_upload + row_update (ya da bir microservice) yazar.`);
377
377
  }
378
- for (const slug of [...(keptSlugs(input.filterable) ?? []), ...(keptSlugs(input.searchable) ?? [])]) {
378
+ // qm S-12: view kaynaklı microservice'te (describe source.textSearch, MicroserviceConnectDataSourceExecutor) alanlar süzülemez
379
+ // ama arama vardır — ConnectDataService request.Search'ü uca geçirir; searchable alanı ise kayıtta 422 alır. Genel
380
+ // "süzülemez/aranamaz" mesajı ajanı çıkmaza sokuyordu. View kaynağında aynı yolu CD-013 söyler (connectDataShapeIssues).
381
+ const textSearchSource = input.source.kind === 'microservice' && describe.source?.textSearch === true;
382
+ for (const slug of [...(keptSlugs(input.filterable) ?? []), ...(textSearchSource ? [] : keptSlugs(input.searchable) ?? [])]) {
379
383
  const field = describe.fields.find(item => item.slug === slug);
380
384
  if (field && (field.canFilter === false || (field.operators ?? []).length === 0)) {
381
385
  block('CD-002', `'${slug}' süzülemez (canFilter yok ya da işleç yok); filterable/searchable'a konamaz.`);
@@ -389,7 +393,11 @@ export function buildDefinition(input, describe, connectable, connectablePaths =
389
393
  }
390
394
  for (const slug of keptSlugs(input.searchable) ?? []) {
391
395
  const field = describe.fields.find(item => item.slug === slug);
392
- if (field && field.canFilter !== false && (field.type !== 'string' || field.isProtected || !(field.operators ?? []).includes('icontains'))) {
396
+ const searchableField = field?.type === 'string' && !field.isProtected && (field.operators ?? []).includes('icontains');
397
+ if (field && textSearchSource && !searchableField) {
398
+ block('CD-002', `'${slug}' aranamaz: '${describe.source?.slug ?? describe.source?.id}' metin aramalı bir kaynak (describe source.textSearch) — arama searchable ile değil, istekteki search ile sunucuda çalışır; platform searchable alanı kayıtta 422 connect-data-source-feature-unsupported ile reddeder. searchable'ı boş bırakın; arama metnini platform istekteki search ile uca geçirir ve uç metni kendisi arar.`);
399
+ }
400
+ else if (field && field.canFilter !== false && !searchableField) {
393
401
  block('CD-002', `'${slug}' aranamaz: arama yalnız korumasız, içerir (icontains) işleci olan metin alanında çalışır — platform bu alanı aramada YOK SAYAR (geçerli hiç alan kalmazsa 422 ConnectDataFilterNotAllowed). Aranabilir bir metin alanı seçin.`);
394
402
  }
395
403
  }
@@ -13,6 +13,7 @@ export const EXPRESSION_PRELUDE_NAMES = [
13
13
  'params',
14
14
  '_PROXY_TAG',
15
15
  '_STATE_PROXY_TAG',
16
+ '_componentItem',
16
17
  'Components',
17
18
  'Page',
18
19
  '_stripProxy',
@@ -471,7 +471,7 @@ export const mobileComponentKnowledge = {
471
471
  differences: [
472
472
  'Matris `toqr` ile üretilir ve düz görünümlerle çizilir (`react-native-svg` yok); hata düzeltme `M` sabittir ve web\'in `errorCorrectionLevel` prop\'u mobil şemadan düşer; renk web gibi siyah üstüne beyaz — koyu temada da (okuyucular ters kodu her zaman okumaz).',
473
473
  'Kare kutunun kenarı: genişliği belli düğümde (`widthType: fill` ya da `fixed`) düğümün genişliği, içerik genişliğinde (`auto` ya da `widthType` hiç yazılmamış) 200 dp; genişlik ölçülemezse de (kanvasta satır kabındaki `fill`) 200 dp — cihaz o düğümü tam genişlik çizer. Sabit px boy kenarı sınırlar. Modül ölçüsü tam sayıya iner, artan birkaç piksel ortalanır. Belirli boy için `appearance.widthType: fixed` + `widthString`.',
474
- 'Veri boşsa ya da QR`a sığmıyorsa (yaklaşık 2,3 KB üstü) yer tutucu çizilir. `quietZone` web ile aynı `common` grubundadır (eski adı `padding` ortak çizim katmanının CSS boşluğuyla çakışıyordu, CQ-272).',
474
+ 'Veri boşsa ya da QR`a sığmıyorsa (yaklaşık 2,3 KB üstü) yer tutucu çizilir. `quietZone` web ile aynı `common` grubundadır (eski adı `padding` ortak çizim katmanının CSS boşluğuyla çakışıyordu, CQ-416).',
475
475
  "`click` bir akışa bağlıysa kod runtime'da basılır (web gibi); bağlı değilse dokunuşu yutmaz. QR'a sığmayan veri `error { errorText }`ü makine başına bir kez yayar (satırdaki seçim değişimi yeniden yaydırmaz)."
476
476
  ],
477
477
  propNotes: {
@@ -494,7 +494,7 @@ export const mobileComponentKnowledge = {
494
494
  'list-item': {
495
495
  summary: 'Liste satırı (FlutterFlow ListTile): baş ikon ya da görsel · başlık · alt başlık · son metin ve ikon; basılabilir. Web`de karşılığı YOKTUR.',
496
496
  differences: [
497
- 'Satıra dokunuş `click` yayar (yük yok). Pasifken soluk ve basılamaz; tasarım kipinde basış kanvasın seçim jestidir.',
497
+ 'Satır yalnız `click` bir akışa BAĞLIYKEN basılır ve `click` yayar (yük yok); bağsız satır düz kutudur ve dokunuşu yutmaz — `repeater`/`grouped-list` şablonunda basış listenin `itemPress`ine gider (2026-10-08). Pasifken soluk ve basılamaz; tasarım kipinde basılmaz (basış kanvasın seçim jestidir).',
498
498
  'Baş tarafta `leadingImage` `leadingIcon`dan önce gelir (40 dp yuvarlak görsel; yüklenmezse başlığın baş harfleri). Boş ikon çizilmez.',
499
499
  "`leadingImage` `image` ile aynı çözümden geçer: kayıttaki FILE alanını nesne olarak verin (`{{ item.foto }}`) ya da `.absolute_path` yazın; `.value` dosya kimliğidir (GUID), görsel çizilmez (MOB-011).",
500
500
  'Ölçüler Material liste satırı: tek satır 56, alt başlıklı 72; `dense` 48 / 60. Renkler token sınıflarından (başlık `--foreground`, alt başlık ve son taraf `--muted-foreground`, basılıyken `--accent`).',
@@ -553,7 +553,7 @@ export const mobileComponentKnowledge = {
553
553
  'Başlık ifadelerde `Section` okur: `{ key, title, count, items }` — ör. `{{ Section.title }} · {{ Section.count }}`. Başlık satır kapsamı TAŞIMAZ (salt okunur, `Row`/`item` boş). `Section` ayrılmış addır: aynı adlı komponent ifade bağlamından süzülür.',
554
554
  'Gruplama: tarih/saat değeri cihazın yerel gününe (`YYYY-AA-GG`) indirgenir, diğer değer metne; başlık = anahtar (biçimli başlık için `Section.key` ile ifade yazın). Alanı boş/eksik satırlar en sondaki `otherSectionTitle` bölümüne düşer.',
555
555
  'Yapışkan başlık (`stickySectionHeaders`) yalnız boyu kabuktan gelen listede çalışır ve cihazda/önizlemede SINIRLI boy ister: `appearance.heightType: fixed` + `heightString` verin. `auto`da liste düz akıştır; sayfanın kaydırıcısı içindeki `fill` içerik kadar uzar, iç kaydırıcı oluşmaz, başlık yapışmaz.',
556
- 'Öğe şablonu basılabilir bir bileşense (`list-item`, `button`) dokunuş onun `click`ine gider ve `itemPress` yayılmaz (repeater ile aynı); `itemPress` için şablonu basılamayan bir kap yapın ya da akışı `list-item.click`e bağlayın (satır seçimi yine olur).',
556
+ 'Öğe şablonu basılabilir bir bileşense (`button`, `click`i bağlı `list-item`) dokunuş onun `click`ine gider ve `itemPress` yayılmaz (repeater ile aynı); `click`i bağlı olmayan `list-item` basılamaz, dokunuş `itemPress`e gider (2026-10-08). Akışı `list-item.click`e bağlarsanız satır seçimi yine olur.',
557
557
  'Çoklu seçim `repeater` ile AYNI sözleşmedir: `selectionMode: multiple` (onay kutusu, dokunuş seçer, `itemPress` yayılmaz), `longPressSelects`, `selectionChange { selectedIndices, count }`, düğümde `selectedIndices`/`selectedItems` — gruplu görev listesinde toplu Ertele/Tamamla.',
558
558
  'v1`de yok: `columnCount`, sanallaştırma, başlıktan yazan akış. Studio`nun satır farkında yerleri (Bind modalı `Row` önerisi, setter satır hedefi) yalnız `repeater`ı tanır — `grouped-list` içinde `Row`/`item` ifadesini elle yazın.'
559
559
  ],
@@ -797,7 +797,7 @@ export const mobileComponentKnowledge = {
797
797
  'Cihazda `react-native-maps`: iOS Apple Haritalar, Android Google Haritalar (Expo Go`da anahtarsız; geliştirme/EAS derlemesinde Android Google anahtarı ve konum izin metinleri gerekir — ARC-128). Kanvas ve önizlemede Leaflet + OpenStreetMap karoları (anahtarsız, köşede OSM atfı; OSM`nin ortak karo sunucusu yoğun trafiğe açık değildir). Kanvasta dokunuş almaz ve sürüklenmez; karolar ve işaretçiler çizilir.',
798
798
  'Görünüm üç sayıdır: `latitude`, `longitude`, `zoom` (web harita ölçeği 1–19, kesirli olabilir; cihazda Apple/Google bölgesine çevrilir). Nesne `center` prop`u YOKTUR: bağlanan düz nesne runtime`da bozulur (ARC-117).',
799
799
  'Akış `latitude`/`longitude`/`zoom` yazınca harita oraya gider (web`de anında, cihazda kısa canlandırmayla) ve `regionChange` YAYMAZ. Kullanıcı sürükleyip yakınlaştırınca bıraktığı görünüm `regionChange { latitude, longitude, zoom }` ile düğüme geri yazılır (`carousel.activeIndex` gibi: prop`a bağlı ifade o an ezilir). Kutu ya da pencere boyunun değişmesi olay sayılmaz.',
800
- 'İşaretçiye dokunmak `markerClick { markerId }` yayar ve başlığı ya da açıklaması varsa balon açar. Harita işaretçiye ORTALANMAZ; kenardaki işaretçinin balonu sığmazsa harita sığdıracak kadar kayar ve bu kayma `regionChange` yayar (iOS ve web`de ölçüldü). Üst üste binen işaretçilerde iOS`ta `markerId` açılan balonunkinden farklı olabilir (ARC-130). İşaretçi rengi temanın ana rengidir. Döndürme ve eğme kapalıdır (ölçek kuzey yukarı görünümün ölçeğidir).',
800
+ 'İşaretçiye dokunmak `markerClick { markerId }` yayar ve başlığı ya da açıklaması varsa balon açar. Harita işaretçiye ORTALANMAZ; kenardaki işaretçinin balonu sığmazsa harita sığdıracak kadar kayar ve bu kayma `regionChange` yayar (iOS ve web`de ölçüldü). Üst üste binen işaretçilerde iOS`ta `markerId` açılan balonunkinden farklı olabilir (ARC-265). İşaretçi rengi temanın ana rengidir. Döndürme ve eğme kapalıdır (ölçek kuzey yukarı görünümün ölçeğidir).',
801
801
  '`interactive: false` sürüklemeyi ve yakınlaştırmayı kapatır, işaretçiler dokunulabilir kalır ve sayfa haritanın üstünden kayar. Açıkken haritanın üstündeki dikey sürükleme haritayı kaydırır, sayfayı değil.',
802
802
  '"Haritada aç" (adres önizlemesi): işaretçi dışına dokunuş `press { latitude, longitude, mapsUrl }` yayar ve düğüme yalnız `mapsUrl` yazılır. Yer tek işaretçiyse o işaretçi (adıyla), değilse görünümün merkezidir. Akış `redirect` `isRaw: true`, `path: \'<harita>.mapsUrl\'` ile açar: iOS Apple Haritalar, Android `geo:` (kullanıcının harita uygulaması), web Google Haritalar. Ayrı bir adım yoktur.',
803
803
  'Kutu `webview` kuralından: kabuktan gelen boy ya da panelin `aspectRatio`su, yoksa 300 dp.'
@@ -811,6 +811,18 @@ export const mobileComponentKnowledge = {
811
811
  showUser: 'Kullanıcının konumunu çizer. Cihazda iOS izni MapKit sorar, Android`de bileşen çalışma anı izni ister; web`de tarayıcı konum izni. Yalnız runtime`da. "Konumum" düğmesi yoktur (Android`inki kamerayı `regionChange` yaymadan taşırdı); konuma gitmek akışın `latitude`/`longitude` yazmasıdır.'
812
812
  }
813
813
  },
814
+ adBanner: {
815
+ summary: 'Google Mobile Ads banner`ı (kök ADR 0012). Web`de karşılığı YOKTUR. Reklam yalnız MAĞAZA sürümünde çizilir; kanvas, önizleme, `/view` ve Zeyrek Mobile oynatıcısı boyutlu kesikli yer tutucu ("Ad · <yerleşim> · 320×50") çizer.',
816
+ differences: [
817
+ '`unit` mağaza ayarındaki reklam yerleşiminin ADIDIR (`mobile_store_update` `settingsPatch.ads.units[].name`), AdMob birim kimliği DEĞİL: kimlik derlemenin platformuna göre manifestten çözülür. Yerleşim `banner` biçiminde olmalı; tam ekran yerleşimi (`interstitial`, `rewarded`…) banner çizmez — onlar `ad-show` adımının işidir.',
818
+ 'Mağaza sürümünde sıra: rıza formu (UMP) → açılış izinleri (iOS ATT) → SDK başlatma; o bitene kadar ve reklam kapalı/rıza yokken bileşen YER KAPLAMAZ ve olay yaymaz. iOS`ta izleme izni verilmemişse reklam kişiselleştirilmeden istenir. Mağaza ayarında `testMode` açıkken Google test birimi çizilir ("Test Ad").',
819
+ 'Yerleşim bu platformda yoksa ya da banner değilse yer kaplamaz ve `fail { code: \'unavailable\' }` yayar; oynatıcıda ve web`de de çalışma kipinde aynı olay yayılır (reklamsız akışı orada da sınayın). Yüklenemeyen reklam (`no-fill`) kutusunu kaldırır: boş kutu kullanıcıya hata gibi görünür.'
820
+ ],
821
+ propNotes: {
822
+ unit: 'Reklam yerleşiminin adı (`^[a-z][A-Za-z0-9]{0,39}$`, mağaza ayarında tanımlı). Panel serbest metindir — yanlış ad cihazda boş kutu + `fail` verir.',
823
+ size: '`banner` 320×50 (varsayılan) · `largeBanner` 320×100 · `mediumRectangle` 300×250 · `fullBanner` 468×60 · `leaderboard` 728×90 (tablet) · `adaptive` kabın genişliğine uyarlı çapa banner · `inlineAdaptive` içerik arası uyarlı. Yer tutucunun ölçüsü bu tablodandır; uyarlanır boyların gerçek yüksekliğini SDK belirler.'
824
+ }
825
+ },
814
826
  datatable: {
815
827
  summary: 'Veri tablosu (FlutterFlow DataTable); prop`lar ve akış sözleşmesi web ikizininki: sıralama ya da sayfa değişince sorgu prop`ları (`sortField`/`sortType`/`offset`) düğüme yazılır ve `mount` yeniden koşar, satır eylemi önce `selectedRow`u yazar sonra eylemin kendi akışını koşturur (yazım web gibi sessizdir, `silentUpdate: [elementId]`: tablo yeniden çizilmez, kaydırma konumu korunur). Hücre biçimi, görünür kolonlar, sıralama döngüsü ve sayfa hesabı web`in modülünden gelir (`renderCell`, `buildViewColumns`, `nextSort`, `computePageState`): iki platform aynı veriden aynı metni üretir.',
816
828
  differences: [
@@ -111,6 +111,26 @@ export const mobileComponentEventContracts = {
111
111
  reads: "işaretçi dışına dokunuşta yayılır (interactive: false önizlemede bütün yüzey). Makine YALNIZ mapsUrl'i düğüme yazar (self.mapsUrl) — koordinat yazılsaydı harita kayardı; koordinat event.data'dadır. Tarif: redirect isRaw: true, path: '<harita>.mapsUrl'"
112
112
  }
113
113
  },
114
+ adBanner: {
115
+ mount: NO_DATA,
116
+ load: {
117
+ payload: {
118
+ width: 'number — yüklenen reklamın genişliği (dp)',
119
+ height: 'number — yüklenen reklamın yüksekliği (dp)'
120
+ },
121
+ reads: 'yalnız mağaza sürümünde, reklam yüklenip çizilince yayılır'
122
+ },
123
+ fail: {
124
+ payload: {
125
+ code: "string — SDK hata kodu (`googleMobileAds/no-fill`, `googleMobileAds/network-error`…) ya da `unavailable`: derleme reklam taşımıyor (oynatıcı, web), ya da yerleşim bu platformda yok / banner değil",
126
+ message: 'string — hatanın açıklaması'
127
+ },
128
+ reads: "reklam çizilemeyince yayılır; kutu yer kaplamaz. Reklam alanını akışla gizlemek ya da yerine içerik koymak için kullanın. Mağaza sürümünde rıza yokken ya da SDK henüz başlamamışken YAYILMAZ"
129
+ },
130
+ click: { payload: {}, reads: 'kullanıcı reklama dokununca (yalnız mağaza sürümü)' },
131
+ open: { payload: {}, reads: 'reklam ekranı kaplayan bir katman açınca' },
132
+ close: { payload: {}, reads: 'kullanıcı reklam katmanından uygulamaya dönünce' }
133
+ },
114
134
  markdown: {
115
135
  mount: NO_DATA,
116
136
  linkClick: {
@@ -160,7 +180,7 @@ export const mobileComponentEventContracts = {
160
180
  index: 'number (öğenin rows içindeki düz indeksi, 0 tabanlı)',
161
181
  row: 'object (öğenin satır verisi)'
162
182
  },
163
- reads: 'yalnız itemPressable açıkken; önce change { selectedIndex } yayılır. Öğe şablonu basılabilir bir bileşense (list-item, button) dokunuş onun click\'ine gider, itemPress yayılmaz'
183
+ reads: 'yalnız itemPressable açıkken; önce change { selectedIndex } yayılır. Öğe şablonu basılabilir bir bileşense (button, click\'i bağlı list-item) dokunuş onun click\'ine gider, itemPress yayılmaz; click\'i bağlı olmayan list-item basılamaz, dokunuş itemPress\'e gider'
164
184
  },
165
185
  swipe: {
166
186
  payload: {
@@ -16,6 +16,14 @@ export const SPLASH_RESIZE_MODES = ['contain', 'cover'];
16
16
  export const RELEASE_STATUSES = ['draft', 'completed'];
17
17
  /** İznin mağaza derlemesindeki kipi (kök ADR 0010): kullanıma göre · her zaman · hiç. Anahtarları backend doğrular. */
18
18
  export const PERMISSION_MODES = ['auto', 'on', 'off'];
19
+ /** Reklam ayarı (kök ADR 0012) — backend `MobileStoreSettingsValidator` / `MobileAdFormat` aynası. */
20
+ export const AD_APP_ID_PATTERN = /^ca-app-pub-\d{16}~\d{10}$/;
21
+ export const AD_UNIT_ID_PATTERN = /^ca-app-pub-\d{16}\/\d{10}$/;
22
+ export const AD_UNIT_NAME_PATTERN = /^[a-z][A-Za-z0-9]{0,39}$/;
23
+ export const AD_UNITS_MAX_COUNT = 40;
24
+ export const AD_FORMATS = ['banner', 'interstitial', 'rewarded', 'rewardedInterstitial', 'appOpen'];
25
+ /** Reklam açıkken taşınması gereken izin: iOS ATT (backend `MobileStoreSettings.AdsPermission`). */
26
+ export const ADS_PERMISSION = 'tracking';
19
27
  export const LISTING_LIMITS = {
20
28
  title: 30,
21
29
  subtitle: 30,
@@ -200,6 +208,36 @@ export function storeSettingsFormatErrors(settings) {
200
208
  add('review.email', 'geçerli e-posta olmalı');
201
209
  if (text(apple.copyright).length > COPYRIGHT_MAX_LENGTH)
202
210
  add('apple.copyright', `en çok ${COPYRIGHT_MAX_LENGTH} karakter`);
211
+ const ads = isObject(settings.ads) ? settings.ads : {};
212
+ const units = (Array.isArray(ads.units) ? ads.units : []).filter(isObject);
213
+ const appIdExample = 'ca-app-pub-0000000000000000~0000000000';
214
+ const unitIdExample = 'ca-app-pub-0000000000000000/0000000000';
215
+ for (const field of ['iosAppId', 'androidAppId']) {
216
+ if (!isBlank(ads[field]) && !AD_APP_ID_PATTERN.test(text(ads[field])))
217
+ add(`ads.${field}`, `AdMob App ID biçiminde olmalı (${appIdExample}; birim kimliğindeki '/' değil '~')`);
218
+ }
219
+ if (ads.enabled === true && isBlank(ads.iosAppId) && isBlank(ads.androidAppId))
220
+ add('ads.enabled', 'reklam açıkken en az bir platformun AdMob App ID\'si gerekir (ads.iosAppId / ads.androidAppId)');
221
+ if (units.length > AD_UNITS_MAX_COUNT)
222
+ add('ads.units', `en çok ${AD_UNITS_MAX_COUNT} yerleşim`);
223
+ const invalidNames = [...new Set(units.map(unit => text(unit.name)).filter(name => !AD_UNIT_NAME_PATTERN.test(name) || units.filter(unit => text(unit.name) === name).length > 1))];
224
+ for (const name of invalidNames)
225
+ add(`ads.units.${name}`, `yerleşim adı ${AD_UNIT_NAME_PATTERN.source} biçiminde ve tekil olmalı (adBanner.unit ve ad-show.value bu adla anar)`);
226
+ for (const unit of units.filter(entry => !invalidNames.includes(text(entry.name)))) {
227
+ const name = text(unit.name);
228
+ if (!AD_FORMATS.includes(text(unit.format)))
229
+ add(`ads.units.${name}.format`, `kabul edilen biçimler: ${AD_FORMATS.join(' | ')}`);
230
+ for (const platform of ['ios', 'android']) {
231
+ if (!isBlank(unit[platform]) && !AD_UNIT_ID_PATTERN.test(text(unit[platform])))
232
+ add(`ads.units.${name}.${platform}`, `AdMob reklam birimi kimliği biçiminde olmalı (${unitIdExample})`);
233
+ }
234
+ if (isBlank(unit.ios) && isBlank(unit.android))
235
+ add(`ads.units.${name}`, 'en az bir platformun reklam birimi kimliği gerekir (ios / android)');
236
+ }
237
+ if (!isBlank(ads.appOpenUnit) && text(units.find(unit => text(unit.name) === text(ads.appOpenUnit))?.format) !== 'appOpen')
238
+ add('ads.appOpenUnit', `'${text(ads.appOpenUnit)}' appOpen biçiminde tanımlı bir yerleşim değil`);
239
+ if (ads.enabled === true && isObject(settings.permissionModes) && settings.permissionModes[ADS_PERMISSION] === 'off')
240
+ add(`permissionModes.${ADS_PERMISSION}`, "reklam açıkken izleme izni (tracking) kapatılamaz — iOS'ta kişiselleştirilmiş reklam ATT ister; kipi auto ya da on yapın");
203
241
  return errors;
204
242
  }
205
243
  /**
@@ -288,6 +326,43 @@ export function permissionLaunchShellWarnings(permissions, shell) {
288
326
  return [];
289
327
  return [`${asked.join(', ')}: açılışta sorulacak diye ayarlı ama yayındaki kabuk paketi (${text(shell.version)}) açılışta izin sormuyor — ayar saklanır, mağaza derlemesi sormaz (ön kontrol P-12 uyarır). Daha yeni bir kabuk yayınlanana kadar izni bir akış adımından isteyin (MOB-025, ${guide}).`];
290
328
  }
329
+ /**
330
+ * MOB-027 — izleme izni (kök ADR 0011): mağaza derlemesi `tracking`'i taşıyorsa App Store Connect App Privacy'de
331
+ * "Device ID — Used to Track You" ve Play Data safety'de "Advertising ID" beyanı insanın işidir; Apple izin penceresinden
332
+ * önce teşvik ya da ön pencereyi yasaklar (5.1.2). Taşınmayan izin için söylenecek bir şey yoktur.
333
+ */
334
+ export function trackingDeclarationWarnings(permissions) {
335
+ const tracking = permissions.find(entry => entry.key === ADS_PERMISSION);
336
+ return isObject(tracking?.build) && tracking.build.carried === true
337
+ ? [`${ADS_PERMISSION}: mağaza derlemesi izleme iznini (ATT) taşıyor — App Store Connect App Privacy'de "Device ID — Used to Track You", Play Console Data safety'de "Advertising ID" beyanı insanın işidir; izin penceresinden önce ödül/teşvik ya da kendi ön pencerenizi göstermeyin (App Store 5.1.2). Sonuçtaki advertisingId yalnız granted iken doludur (MOB-027, ${guide}).`]
338
+ : [];
339
+ }
340
+ /**
341
+ * MOB-026 — reklam açıkken ajanın bilmesi gereken, kayıttan okunabilen durumlar (kök ADR 0012). Satırlar `<konu>: …`
342
+ * biçimindedir. Test kipi mağaza yüklemesinde de test reklamı gösterir (ön kontrol P-13 `manual`); yerleşimsiz reklam
343
+ * SDK'yı bağlar ama hiçbir şey göstermez; App ID'si verilmiş platformda birimi olmayan yerleşim o platformda yer kaplamaz
344
+ * (`adBanner`) ya da `invalid` döner (`ad-show`). Beyan satırı her zaman gelir: App Privacy, Data safety ve AdMob rıza
345
+ * mesajı konsolda insanın işidir, gateway yazamaz.
346
+ */
347
+ export function adsStoreWarnings(settings) {
348
+ const ads = isObject(settings.ads) ? settings.ads : {};
349
+ if (ads.enabled !== true)
350
+ return [];
351
+ const units = (Array.isArray(ads.units) ? ads.units : []).filter(isObject);
352
+ const platforms = [['ios', 'iOS', ads.iosAppId], ['android', 'Android', ads.androidAppId]];
353
+ const lines = [];
354
+ if (ads.testMode !== false)
355
+ lines.push(`testMode: test kipi açık — mağaza yüklemesinde de gerçek reklam yerine Google test reklamı gösterilir; yayından önce ads.testMode: false (ön kontrol P-13, MOB-026, ${guide}).`);
356
+ if (units.length === 0)
357
+ lines.push(`yerleşim: reklam açık ama hiç yerleşim yok — SDK bağlanır, hiçbir reklam gösterilmez; ads.units'e banner ya da tam ekran yerleşim ekleyin (MOB-026, ${guide}).`);
358
+ for (const unit of units) {
359
+ const missing = platforms.filter(([field, , appId]) => !isBlank(appId) && isBlank(unit[field])).map(([, label]) => label);
360
+ if (missing.length > 0)
361
+ lines.push(`${text(unit.name)}: ${missing.join(' ve ')}'de birim yok — o platformda adBanner yer kaplamaz, ad-show 'invalid' döner (MOB-026).`);
362
+ }
363
+ lines.push(`beyan: reklam izleme sayılır — App Store Connect App Privacy'de "Device ID — Used to Track You" (ve reklam verisi), Play Console Data safety'de "Advertising ID", AdMob konsolunda rıza (GDPR/UMP) mesajı insanın işidir; gateway yazamaz. Beyanı kullanıcı onaylayınca mobile_store_update { declarations } (MOB-026, ${guide}).`);
364
+ return lines;
365
+ }
291
366
  /** MOB-022 — `definition` birleşmiş kayıttır; biçim hatası backend'in 422'siyle aynı ret (block), eksik alan uyarı. */
292
367
  export const mobileStorePolicyRules = [
293
368
  {
@@ -4,6 +4,7 @@ import { ensureWorkflowErrorPath } from './errorPath.js';
4
4
  import { DOCUMENT_STYLE_TYPES, TYPED_STYLE_COMPONENTS } from './styleResolve.js';
5
5
  import { STATE_STORAGE_KINDS, STEP_ALIASES } from './platform/profile.js';
6
6
  import { inlineStyleFields } from './componentCatalog.js';
7
+ import { FUNCTIONS_NAMESPACE } from './functionGuards.js';
7
8
  const SORT_DEFAULT = { name: 'sortField', type: 'sortType', source: 'query-string' };
8
9
  const LIMIT_DEFAULT = { name: 'limit', offset: 'offset', source: 'query-string' };
9
10
  function asArray(value) {
@@ -362,6 +363,7 @@ const REQUIRED_COLLECTIONS = ['events', 'validations', 'stateBindings', 'childre
362
363
  *
363
364
  * - `states`: DesignComponentManagementSchema.ToDomain:47 `States.IsSticky`'yi guard'sız okur;
364
365
  * Appearance'ın satır 24'te varsayılanı var, States'in yok.
366
+ * - `blocks` doldurulmaz: konum ebeveynin düzenine bağlıdır, eksikse PAGE-037 ile reddedilir.
365
367
  * - `events` / `validations` / `stateBindings`: yalnız **micro-op yolunda** zorunlu.
366
368
  * `page_create` (POST /pages) bunlarsız 200 döner, `PATCH …/schema` NRE verir; canlı deneyde
367
369
  * üçü birlikte verilince geçti, her biri tek başına verilince patladı (klinik H-005 eki).
@@ -675,6 +677,11 @@ export function styleKeyNote(fixed) {
675
677
  + 'değer kaydedilir ama hiç uygulanmaz ve tek işareti tarayıcı konsoludur (F-06). '
676
678
  + "Seçici anahtarlara ('&__body', ':hover', '--token') dokunulmadı."];
677
679
  }
680
+ /** PAGE-009 ile aynı kural: JS tanımlayıcısı (tireler çıkarılınca) ve ayrılmış `Functions` değil. */
681
+ function isDerivableComponentName(name) {
682
+ const bare = name.replace(/-/g, '');
683
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(bare) && bare !== FUNCTIONS_NAMESPACE;
684
+ }
678
685
  /**
679
686
  * Kabın `states.isDroppable`ı KATALOĞUN kararıdır. Gateway düğümü kurarken bu alanı
680
687
  * yazmıyordu (`DEFAULT_STATES` hepsini `false` bırakır) ve Studio kanvası düğümü kap saymıyordu:
@@ -721,6 +728,7 @@ export function droppableStatesNote(fixed) {
721
728
  * hiç denetlemez (`PageSchemaValidator.cs`); Studio her düğüme `name = elementId` yazar
722
729
  * (`ScreenBuilderDroppable.svelte`). Kural deterministik olduğu için reddetmek yerine dolduruyoruz.
723
730
  * Doldurulan ad da PAGE-009'dan geçmek zorunda: çağıran bunu denetimden ÖNCE koşar. Dolu ada dokunulmaz.
731
+ * `normalizeComponentDefaults` aynı boşluğu yalnız geçerli bir elementId için ikinci kez kapatır (güvenlik ağı).
724
732
  */
725
733
  export function fillComponentNames(nodes) {
726
734
  const filled = [];
@@ -745,6 +753,16 @@ export function normalizeComponentDefaults(nodes) {
745
753
  const fixed = [];
746
754
  for (const node of asObjects(nodes)) {
747
755
  const missing = [];
756
+ // name'siz düğümü platform kabul eder ama sayfada bir akış ya da `{{ }}` kod prop'u varsa
757
+ // FlowEngine runCode "Cannot read properties of undefined (reading 'catch')" ile düşer, akış
758
+ // `running:1`'de takılır ve hata adsız düğümü işaret etmez (image ölçüm turu, 2026-10-04).
759
+ // Yalnız PAGE-009'un geçerli saydığı elementId'den türetilir: page_create/page_update/layout
760
+ // yollarında PAGE-009 denetimi bu normalizasyondan ÖNCE koşar; bozuk bir elementId'den türeyen
761
+ // ad denetimi atlatıp "Missing initializer in const declaration" ile sayfayı öldürürdü.
762
+ if ((typeof node.name !== 'string' || node.name.trim() === '') && typeof node.elementId === 'string' && isDerivableComponentName(node.elementId)) {
763
+ node.name = node.elementId;
764
+ missing.push('name');
765
+ }
748
766
  if (!node.states || typeof node.states !== 'object') {
749
767
  node.states = { ...DEFAULT_STATES };
750
768
  missing.push('states');
@@ -1012,7 +1030,7 @@ export const PAGE_CONTAINER_ELEMENT_ID = 'page_container';
1012
1030
  * Düzen prop'ları da kurulur (2026-08-25): `ContainerComponent` şemasının `layout` varsayılanı
1013
1031
  * `grid` → `flex` oldu ve Studio bırakma yolu katalogdaki `definition.defaults`'u düğümün
1014
1032
  * props'una YAZAR (`ScreenBuilderDroppable.svelte`); sayfa kökünü kuran öbür üreticiler de aynı seti
1015
- * yazar (Studio `DEFAULT_CONTAINER_LAYOUT_PROPS`, studio-api `generatePageContainer`, platform
1033
+ * yazar (Studio `DEFAULT_CONTAINER_LAYOUT_PROPS`, platform
1016
1034
  * `CreatePageWhenApplicationCreatedEventHandler` + `SeedData`). Renderer varsayılanı
1017
1035
  * BİLMEZ — `isFlexLayout` yalnız düğümün kendi prop'una bakar — bu yüzden gateway'in kurduğu
1018
1036
  * kapsayıcı da seti açıkça taşır; yoksa Studio'dan açılan sayfa flex, MCP'den açılan ızgara olurdu.
@@ -1502,10 +1520,9 @@ function collectLayout(nodes, collected) {
1502
1520
  */
1503
1521
  const MAX_REORDER_PASSES = 8;
1504
1522
  /**
1505
- * Studio'da komponent tuvale bırakıldığında ızgara çakışmasını studio-api'nin sayfa kurma servisi
1506
- * çözer: `SchemaHelper.mergeComponents` yeni komponenti kardeşlerine ekleyip `reorderComponents` →
1507
- * `draggableElement.moveItem` ile çakışan kardeşleri aşağı iter (NgApps.UI
1508
- * `apps/studio-api/src/domain/page/shared/page-schema.helper.ts:28`). Gateway o servisten geçmez —
1523
+ * Studio'da komponent tuvale bırakıldığında ızgara çakışmasını Studio'nun ızgara motoru çözer:
1524
+ * `moveItem` çakışan kardeşleri aşağı iter (`@ng-apps/screen-builder`
1525
+ * `src/draggable/draggable.element.ts`). Gateway o motordan geçmez —
1509
1526
  * `page_create`/`page_update` şemayı POST/PUT ile KOMPLE yazar (`page_build` tool'u yok) — bu yüzden
1510
1527
  * ajanın verdiği koordinatlar olduğu gibi kayda giriyor ve aynı hücreleri isteyen komponentler
1511
1528
  * Studio tuvalinde üst üste biniyor. Burada aynı algoritma şemanın TAMAMINA uygulanır: en dıştaki
@@ -116,7 +116,7 @@ export const mobileProfile = Object.freeze({
116
116
  },
117
117
  style: { freeStyle: false, hoverStyle: false, resolver: 'native' },
118
118
  steps: {
119
- registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'navigate', 'storage', 'setcookie', 'download', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings']),
119
+ registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'navigate', 'storage', 'setcookie', 'download', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show']),
120
120
  aliases: STEP_ALIASES,
121
121
  infoOnly: new Set(['download', 'setcookie'])
122
122
  },
@@ -180,7 +180,7 @@ export const MOBILE_WEB_ONLY = {
180
180
  'layout-region': "tek slot'lu sıradan layout",
181
181
  'auth-layout': "tek slot'lu sıradan layout",
182
182
  popover: "'modal' widget'ı",
183
- 'theme-toggle': "uygulama içi tema ayarı: 'choice-chips' (variant: segmented, Sistem/Açık/Koyu) → storage adımı name: 'zeyrek:theme-mode', value: system | light | dark; barındırıcı temayı anında değiştirir, cihazda kalıcı (seçim yazılmadıysa cihaz teması)",
183
+ 'theme-toggle': "uygulama içi tema ayarı: 'choice-chips' (variant: segmented, Sistem/Açık/Koyu) → storage adımı name: 'zeyrek:theme-mode', value: system | light | dark; barındırıcı temayı anında değiştirir, cihazda kalıcı (seçim yazılmadıysa açık tema; cihaz teması yalnız system seçilince)",
184
184
  pagination: "'repeater' + daha fazla yükle düğmesi",
185
185
  breadcrumb: "yığın gezinmesi (app-bar geri düğmesi)",
186
186
  fileUpload: "'fileInput' (alan) ya da 'filebutton' (düğme) — dokunmatikte sürükle-bırak yok; ikisi de aynı base64 değeri yazar"
@@ -20,7 +20,7 @@ export const webProfile = Object.freeze({
20
20
  },
21
21
  style: { freeStyle: true, hoverStyle: true, resolver: 'css' },
22
22
  steps: {
23
- registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings']),
23
+ registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show']),
24
24
  aliases: STEP_ALIASES,
25
25
  infoOnly: new Set()
26
26
  },
@@ -168,6 +168,7 @@ const flowBranchNames = {
168
168
  bpm: ['always', 'success', 'fail'],
169
169
  permission: ['always'],
170
170
  'open-settings': ['always'],
171
+ 'ad-show': ['always', 'error'],
171
172
  end: []
172
173
  };
173
174
  /**
@@ -180,7 +181,7 @@ export const knownFlowStepTypes = new Set([
180
181
  'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
181
182
  'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function', 'share',
182
183
  'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'bpm',
183
- 'permission', 'open-settings'
184
+ 'permission', 'open-settings', 'ad-show'
184
185
  ]);
185
186
  /**
186
187
  * Cihaz yeteneği adımlarının kapalı kümeleri — `@ng-apps/flow` `tasks/device/device.functions.ts`
@@ -189,6 +190,11 @@ export const knownFlowStepTypes = new Set([
189
190
  */
190
191
  export const hapticStyles = new Set(['success', 'warning', 'error', 'light', 'medium', 'heavy', 'selection']);
191
192
  export const locationAccuracies = new Set(['low', 'balanced', 'high']);
193
+ /**
194
+ * Reklam yerleşiminin adı — backend `MobileStoreSettingsValidator.AdUnitNamePattern`'ın aynası (kök ADR 0012); `ad-show.value`
195
+ * ve `adBanner.unit` bu adla mağaza ayarındaki yerleşimi anar.
196
+ */
197
+ export const adPlacementNamePattern = /^[a-z][A-Za-z0-9]{0,39}$/;
192
198
  /** `permission` adımının kipleri — `@ng-apps/flow` `PERMISSION_MODES`'un aynası (kök ADR 0010); tür adını gateway bilmez, backend doğrular. */
193
199
  export const permissionStepModes = new Set(['request', 'check']);
194
200
  const flowFreeBranchTypes = new Set(['condition']);
@@ -2626,6 +2632,54 @@ export const policyRules = [
2626
2632
  return findings;
2627
2633
  })
2628
2634
  },
2635
+ {
2636
+ id: 'FLOW-039',
2637
+ module: 'flow',
2638
+ appliesTo: ['create', 'update'],
2639
+ sourceRef: "Tam ekran reklam adımının argümanı StepManagementArgument.value'dur (kök docs/adr/0012-reklam-yerlesimleri-magaza-ayari-ve-kosullu-sdk.md): mağaza ayarındaki reklam yerleşiminin ADI, DÜZ METİN — AdMob birim kimliği değil (kimlik derlemenin platformuna göre manifestten çözülür). Backend FlowDtoValidator HasAdPlacement adı ^[a-z][A-Za-z0-9]{0,39}$ deseniyle denetler, uymayan 422 alır; unit/placement/adUnitId gibi anahtarlar sözleşmede YOKTUR ve 200 ile sessizce düşer. Yerleşimin mağaza ayarında tanımlı olduğu burada denetlenemez: mobile_store_get",
2640
+ check: definition => flowAllSteps(definition)
2641
+ .filter(step => step.type === 'ad-show')
2642
+ .flatMap((step) => {
2643
+ const args = (step.args ?? {});
2644
+ const unit = typeof args.value === 'string' ? args.value : '';
2645
+ const unquoted = unit.replace(/^['"`](.*)['"`]$/, '$1');
2646
+ const stray = ['unit', 'placement', 'adUnitId', 'unitId', 'format'].filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
2647
+ const findings = [];
2648
+ if (!unit.trim()) {
2649
+ findings.push({
2650
+ ruleId: 'FLOW-039',
2651
+ severity: 'block',
2652
+ message: `'${flowStepLabel(step)}' ad-show adımının yerleşimi yok: args.value mağaza ayarındaki tam ekran yerleşimin ADINI ister (platform 422 döner). Yerleşimler: mobile_store_get — ads.units[] (format interstitial, rewarded ya da rewardedInterstitial).`,
2653
+ guide: 'ngapps://guides/mobile'
2654
+ });
2655
+ }
2656
+ else if (unquoted !== unit) {
2657
+ findings.push({
2658
+ ruleId: 'FLOW-039',
2659
+ severity: 'block',
2660
+ message: `'${flowStepLabel(step)}' ad-show adımının yerleşimi tırnaklı yazılmış: ${unit}. Bu alan DÜZ METİNDİR, ifade değil — args.value: '${unquoted}' yazın.`,
2661
+ guide: 'ngapps://guides/mobile'
2662
+ });
2663
+ }
2664
+ else if (!adPlacementNamePattern.test(unit)) {
2665
+ findings.push({
2666
+ ruleId: 'FLOW-039',
2667
+ severity: 'block',
2668
+ message: `'${flowStepLabel(step)}' ad-show adımının yerleşimi bir yerleşim adı değil: '${unit}'. Değer ifade ya da AdMob birim kimliği olamaz; mağaza ayarındaki yerleşimin adıdır (${adPlacementNamePattern.source}). Platform 422 döner. Yerleşimler: mobile_store_get — ads.units[].name.`,
2669
+ guide: 'ngapps://guides/mobile'
2670
+ });
2671
+ }
2672
+ if (stray.length > 0) {
2673
+ findings.push({
2674
+ ruleId: 'FLOW-039',
2675
+ severity: unit.trim() ? 'warn' : 'block',
2676
+ message: `'${flowStepLabel(step)}' ad-show adımında args.${stray.join('/args.')} alanı var: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu 200 ile sessizce düşürür. Yerleşimin adı args.value'ya düz metin olarak yazılır — ör. args: { value: '<yerleşim adı>' }.`,
2677
+ guide: 'ngapps://guides/mobile'
2678
+ });
2679
+ }
2680
+ return findings;
2681
+ })
2682
+ },
2629
2683
  {
2630
2684
  id: 'FLOW-038',
2631
2685
  module: 'flow',
@@ -3086,6 +3140,29 @@ export const policyRules = [
3086
3140
  }];
3087
3141
  })
3088
3142
  },
3143
+ {
3144
+ id: 'FLOW-027',
3145
+ module: 'flow',
3146
+ appliesTo: ['create', 'update'],
3147
+ sourceRef: "2026-10-01 kanban ölçüm turu: steps ve errorSteps'te AYNI stepId ('start') flow_create'te 500 `An item with the same key has already been added. Key: start [ArgumentException]` verdi — platform adımları tek sözlüğe stepId anahtarıyla koyuyor, hata mesajı çakışmanın stepId olduğunu söylemiyor",
3148
+ check: definition => {
3149
+ const counts = new Map();
3150
+ for (const step of flowAllSteps(definition)) {
3151
+ if (!step.stepId)
3152
+ continue;
3153
+ counts.set(step.stepId, (counts.get(step.stepId) ?? 0) + 1);
3154
+ }
3155
+ const repeated = [...counts].filter(([, count]) => count > 1).map(([stepId]) => stepId);
3156
+ return repeated.length === 0
3157
+ ? []
3158
+ : [{
3159
+ ruleId: 'FLOW-027',
3160
+ severity: 'block',
3161
+ message: `stepId'ler akışta (steps + errorSteps birlikte) benzersiz olmalı; tekrar edenler: ${repeated.map(id => `'${id}'`).join(', ')}. Platform bunları tek sözlüğe anahtar olarak koyar ve "An item with the same key has already been added" ile 500 verir (mesaj stepId'den söz etmez). errorSteps'te de kendi 'start' adımı gerekiyorsa ona farklı bir stepId verin (örn. 'err_start').`,
3162
+ guide: 'ngapps://guides/page'
3163
+ }];
3164
+ }
3165
+ },
3089
3166
  {
3090
3167
  id: 'MS-013',
3091
3168
  module: 'microservice',
@@ -3309,7 +3386,7 @@ export const policyRules = [
3309
3386
  id: 'MW-003',
3310
3387
  module: 'workflow',
3311
3388
  appliesTo: ['create', 'update', 'preview'],
3312
- sourceRef: "set-user değişkeni istek kapsamındaki WorkflowContext'e yazılır (SetUserStep → AddVariable('user')) ve sonraki middleware'in motoru onu görür (WorkflowRunner her koşuda JavascriptEngineInstance'ı _variables'tan kurar; sıra ExecuteMicroserviceBehavior:96 / ExecutePageBehavior:121 → OrderBy(m => m.Name)). Ama kuran middleware set-user'a ULAŞMADAN biterse (çerez yok, koşul dalı) değişken hiç doğmaz ve okuyan middleware ham 'user is not defined' ile 500 verir — 401 değil (crm koşumu B-3)",
3389
+ sourceRef: "set-user değişkeni istek kapsamındaki WorkflowContext'e yazılır (SetUserStep → AddVariable('user')) ve sonraki middleware'in motoru onu görür (WorkflowRunner her koşuda CreateJavascriptEngine() ile motoru _variables'tan kurar; sıra ExecuteMicroserviceBehavior:96 / ExecutePageBehavior:121 → OrderBy(m => m.Name)). Ama kuran middleware set-user'a ULAŞMADAN biterse (çerez yok, koşul dalı) değişken hiç doğmaz ve okuyan middleware ham 'user is not defined' ile 500 verir — 401 değil (crm koşumu B-3)",
3313
3390
  check: (definition, context) => {
3314
3391
  if (context.workflowKind !== 'middleware')
3315
3392
  return [];