@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.
- package/dist/knowledge/componentCatalog.js +389 -149
- package/dist/knowledge/componentEvents.js +16 -8
- package/dist/knowledge/connectDataBind.js +10 -2
- package/dist/knowledge/expressionPrelude.js +1 -0
- package/dist/knowledge/mobileComponentCatalog.js +16 -4
- package/dist/knowledge/mobileComponentEvents.js +21 -1
- package/dist/knowledge/mobileStore.js +75 -0
- package/dist/knowledge/normalize.js +22 -5
- package/dist/knowledge/platform/mobile.js +1 -1
- package/dist/knowledge/platform/mobileRules.js +1 -1
- package/dist/knowledge/platform/web.js +1 -1
- package/dist/knowledge/rules.js +79 -2
- package/dist/knowledge/styleResolve.js +27 -7
- package/dist/knowledge/styleResolveNative.js +4 -2
- package/dist/knowledge/themeResolve.js +1 -1
- package/dist/knowledge/themeSchema.js +2 -1
- package/dist/observability/trace.js +7 -2
- package/dist/platform/secrets.js +50 -2
- package/dist/resources/guides/component.md +1 -1
- package/dist/resources/guides/merge.md +5 -4
- package/dist/resources/guides/microservice.md +1 -1
- package/dist/resources/guides/middleware.md +1 -1
- package/dist/resources/guides/mobile-publish.md +24 -0
- package/dist/resources/guides/mobile.md +22 -6
- package/dist/resources/guides/page.md +14 -7
- package/dist/resources/guides/theme.md +1 -0
- package/dist/resources/schema-fingerprint.json +3 -3
- package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +2 -1
- package/dist/resources/schemas/OrganizationProjectDto.json +8 -0
- package/dist/resources/schemas/UpsertMobileStoreSettingsCommand.json +59 -0
- package/dist/tools/applications.js +6 -3
- package/dist/tools/common.js +9 -5
- package/dist/tools/environments.js +1 -1
- package/dist/tools/flows.js +5 -1
- package/dist/tools/mobileStore.js +21 -5
- package/dist/tools/pages.js +3 -3
- 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
|
|
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: '
|
|
121
|
-
edit: { payload: { id: 'string', text: 'string (yeni metin)', author: 'string (currentUser — işlemi yapan)' }, reads: '
|
|
122
|
-
delete: { payload: { id: 'string', author: 'string (currentUser — işlemi yapan)' }, reads: '
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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-
|
|
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
|
-
'
|
|
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
|
|
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-
|
|
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
|
|
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`,
|
|
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ı
|
|
1506
|
-
*
|
|
1507
|
-
* `
|
|
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
|
},
|
package/dist/knowledge/rules.js
CHANGED
|
@@ -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
|
|
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 [];
|