@appaflytech/zeyrek-ai-mcp 0.0.22 → 0.0.24

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 (50) hide show
  1. package/.env.example +15 -0
  2. package/dist/config.js +9 -0
  3. package/dist/index.js +2 -0
  4. package/dist/knowledge/componentCatalog.js +4 -2
  5. package/dist/knowledge/componentEvents.js +3 -3
  6. package/dist/knowledge/mobileComponentCatalog.js +308 -50
  7. package/dist/knowledge/mobileComponentEvents.js +130 -3
  8. package/dist/knowledge/normalize.js +16 -6
  9. package/dist/knowledge/platform/lint.js +110 -3
  10. package/dist/knowledge/platform/mobile.js +1 -1
  11. package/dist/knowledge/platform/mobileRules.js +70 -11
  12. package/dist/knowledge/platform/web.js +1 -1
  13. package/dist/knowledge/rules.js +106 -11
  14. package/dist/knowledge/styleResolve.js +49 -3
  15. package/dist/knowledge/styleResolveNative.js +104 -4
  16. package/dist/observability/trace.js +1 -1
  17. package/dist/platform/argumentAliases.js +128 -0
  18. package/dist/platform/bodyFile.js +103 -0
  19. package/dist/platform/capture/capture.js +29 -5
  20. package/dist/platform/capture/console-log.js +80 -0
  21. package/dist/platform/capture/index.js +1 -0
  22. package/dist/platform/diagnostics.js +10 -4
  23. package/dist/platform/http.js +23 -7
  24. package/dist/platform/responses.js +6 -2
  25. package/dist/platform/schemaReadMemory.js +60 -0
  26. package/dist/platform/secrets.js +9 -0
  27. package/dist/platform/strictArguments.js +68 -12
  28. package/dist/platform/tokenVault.js +187 -4
  29. package/dist/resources/guides/expressions.md +1 -1
  30. package/dist/resources/guides/microservice.md +9 -2
  31. package/dist/resources/guides/middleware.md +5 -1
  32. package/dist/resources/guides/mobile.md +165 -10
  33. package/dist/resources/guides/page.md +161 -41
  34. package/dist/resources/guides/theme.md +4 -1
  35. package/dist/resources/guides/workflow.md +7 -1
  36. package/dist/resources/schema-fingerprint.json +2 -2
  37. package/dist/server.js +1 -1
  38. package/dist/tools/datasources.js +1 -1
  39. package/dist/tools/designSchemaPatch.js +4 -3
  40. package/dist/tools/externalComponents.js +1 -1
  41. package/dist/tools/flows.js +70 -4
  42. package/dist/tools/layouts.js +5 -5
  43. package/dist/tools/microservices.js +26 -10
  44. package/dist/tools/pages.js +192 -36
  45. package/dist/tools/platformInfo.js +4 -1
  46. package/dist/tools/rows.js +7 -1
  47. package/dist/tools/schemaOps.js +1 -1
  48. package/dist/tools/screenWrite.js +4 -3
  49. package/dist/tools/screens.js +81 -0
  50. package/package.json +1 -1
@@ -12,7 +12,23 @@ export const mobileComponentEventContracts = {
12
12
  'choice-chips': {
13
13
  change: {
14
14
  payload: {
15
- value: 'string — seçili çipin değeri. `multiple` açıkken seçili değerler virgülle ayrılmış tek dize olarak gelir.'
15
+ value: 'string | number | boolean | Array — seçenek ÖZGÜN tipinde (değeri `34` yazılmış seçenek sayı yayar). `multiple` açıkken seçili değerlerin DİZİSİ (`[34, "a"]`); tek kipte seçili çipe yeniden dokunmak boş metin (`""`) yayar. Devre dışı seçenek olay yaymaz.'
16
+ }
17
+ }
18
+ },
19
+ // Web ikizinin yükü metindir (`e.currentTarget.value`); mobil seçeneği ÖZGÜN tipinde yayar (f-06, `optionEmitValues`).
20
+ // İki platformda aynı koşul için karşılaştırmayı metne çevirin: `String(self.value) === '34'`.
21
+ radio: {
22
+ change: {
23
+ payload: {
24
+ value: 'string | number | boolean — seçilen seçeneğin ÖZGÜN değeri (değeri `34` yazılmış seçenek sayı yayar; web aynı seçimi `"34"` metni olarak yayar)'
25
+ }
26
+ }
27
+ },
28
+ checkboxgroup: {
29
+ change: {
30
+ payload: {
31
+ value: 'Array — seçili seçeneklerin ÖZGÜN değerleri (`["su", 9]`; web metin dizisi yayar). Seçeneklerde olmayan eski değer metin kalır'
16
32
  }
17
33
  }
18
34
  },
@@ -56,9 +72,31 @@ export const mobileComponentEventContracts = {
56
72
  mount: NO_DATA,
57
73
  load: { payload: {}, reads: "her tamamlanan yüklemede yayılır (izin verilen gezinmeden sonra da); cihazda yüklenemeyen sayfada yayılmaz (yerine error), web'de tarayıcı engellenen çerçevede de yayar" },
58
74
  navigate: { payload: { url: "string — sayfanın gitmek istediği adres; makine url prop'una yazar (self.url). allowNavigation kapalıyken (varsayılan) gezinme engellenir; adres yalnız kullanıcının dokunduğu bağlantıda sistemde açılır" } },
59
- message: { payload: { data: "string — sayfanın window.ReactNativeWebView.postMessage(metin) ile yolladığı metin, olduğu gibi; makine data prop'una yazar (self.data)" } },
75
+ message: {
76
+ payload: { data: "string — sayfanın window.ReactNativeWebView.postMessage(metin) ile yolladığı metin, olduğu gibi; makine data prop'una yazar (self.data)" },
77
+ reads: "iki yönlü köprü: sayfa window.ngApps.emit(name, data) çağırırsa yük olayın params'ında gelir — akış event.name ve event.data okur (self.data yazılmaz)"
78
+ },
60
79
  error: { payload: {}, reads: "cihazda sayfa yüklenemeyince (ağ hatası) yayılır, görünüm yer tutucu çizer; web'de pratikte gelmez (iframe gibi)" }
61
80
  },
81
+ html: {
82
+ mount: NO_DATA,
83
+ message: {
84
+ payload: {},
85
+ reads: "belgenin window.ngApps.emit(name, data) çağrısı; yük olayın params'ındadır — akış event.name (ileti adı) ve event.data (veri) okur. actions satırının akışı ise fireEvent(key, data) ile koşar: event.name = key, event.data = veri"
86
+ }
87
+ },
88
+ map: {
89
+ mount: NO_DATA,
90
+ markerClick: { payload: { markerId: "string — dokunulan işaretçinin `id`si (boşsa listedeki sırası); makine markerId prop'una yazar (self.markerId). Başlığı ya da açıklaması olan işaretçide balon da açılır" } },
91
+ regionChange: {
92
+ payload: {
93
+ latitude: 'number — kullanıcının bıraktığı merkezin enlemi (6 basamak)',
94
+ longitude: 'number — merkezin boylamı (6 basamak, −180…180)',
95
+ zoom: 'number — web harita ölçeği (2 basamak, 1–19)'
96
+ },
97
+ reads: "yalnız kullanıcının sürüklemesi ya da yakınlaştırması bitince yayılır; akışın yazdığı görünüm ve kutu boyu değişimi yaymaz. Makine üç prop'u da yazar (self.latitude, self.longitude, self.zoom)"
98
+ }
99
+ },
62
100
  markdown: {
63
101
  mount: NO_DATA,
64
102
  linkClick: {
@@ -75,10 +113,92 @@ export const mobileComponentEventContracts = {
75
113
  'safe-area': {
76
114
  mount: NO_DATA
77
115
  },
116
+ // Web `container` yalnız `mount` yayar; `click` mobile-only (plans/mobil-plantcare-acigi f-04, K1).
117
+ container: {
118
+ click: NO_DATA
119
+ },
120
+ // Arama varyantının "Vazgeç"i (plans/mobil-plantcare-acigi f-05): değer temizlenir, klavye kapanır.
121
+ textinput: {
122
+ cancel: NO_DATA
123
+ },
78
124
  'scroll-view': {
79
125
  mount: NO_DATA,
80
126
  scrollEnd: NO_DATA,
81
- refresh: NO_DATA
127
+ refresh: NO_DATA,
128
+ scroll: {
129
+ payload: {
130
+ y: 'number — dikey kaydırma konumu (yatay kaydırıcıda x)',
131
+ contentHeight: 'number — içeriğin boyu (yatayda contentWidth)',
132
+ viewportHeight: 'number — görünen alanın boyu (yatayda viewportWidth)'
133
+ },
134
+ reads: '100 ms seyreltmeli; yalnız bu olaya akış bağlıyken yayılır. Düğüme scrollY (scrollX) de yazılır'
135
+ }
136
+ },
137
+ 'grouped-list': {
138
+ mount: NO_DATA,
139
+ change: {
140
+ payload: {
141
+ selectedIndex: 'number — dokunulan öğenin rows içindeki DÜZ indeksi (bölüm içi değil); aynı öğeye yeniden dokununca yayılmaz. Makine selectedIndex\'i yazar, akışta tıklanan satır <liste>.selectedItem'
142
+ }
143
+ },
144
+ itemPress: {
145
+ payload: {
146
+ index: 'number (öğenin rows içindeki düz indeksi, 0 tabanlı)',
147
+ row: 'object (öğenin satır verisi)'
148
+ },
149
+ 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'
150
+ },
151
+ swipe: {
152
+ payload: {
153
+ key: 'string (kaydırma eyleminin key alanı)',
154
+ index: 'number (öğenin rows içindeki düz indeksi, 0 tabanlı)',
155
+ row: 'object (öğenin satır verisi)'
156
+ }
157
+ },
158
+ sectionPress: {
159
+ payload: {
160
+ key: 'string — bölüm anahtarı (tarih değerinde yerel gün YYYY-AA-GG)',
161
+ title: 'string — bölüm başlığı (anahtar; boş alanlı satırların bölümünde otherSectionTitle)',
162
+ count: 'number — bölümdeki öğe sayısı',
163
+ items: 'object[] — bölümdeki satırların verisi'
164
+ },
165
+ reads: 'bölüm başlığına dokununca; yük başlık şablonunun Section ifadesiyle aynı nesnedir'
166
+ },
167
+ selectionChange: {
168
+ payload: {
169
+ selectedIndices: 'number[] — seçili satırların rows içindeki DÜZ indeksleri, sıralı',
170
+ count: 'number — seçili satır sayısı'
171
+ },
172
+ reads: 'yalnız selectionMode: multiple (ya da longPressSelects ile açılınca); akış event.data.selectedIndices / event.data.count okur, düğümün selectedIndices/selectedItems değeri de yazılır'
173
+ }
174
+ },
175
+ eventcalendar: {
176
+ mount: NO_DATA,
177
+ dateClick: {
178
+ payload: { date: "string 'yyyy-MM-dd'", rows: 'object[] — o güne düşen kaynak satırlar (mobil)' },
179
+ reads: "self'e yazılmaz — event.data ile oku; ay ve hafta görünümünde güne dokununca"
180
+ },
181
+ eventClick: {
182
+ payload: { id: 'string', title: 'string', row: 'kaynak satırın TAMAMI' },
183
+ reads: "self'e yazılmaz — event.data ile oku; gün çizelgesinde bloğa dokununca"
184
+ },
185
+ monthChange: {
186
+ payload: {
187
+ month: 'number (1-TABANLI)',
188
+ year: 'number',
189
+ view: "'month' | 'week' | 'day'",
190
+ start: "string 'yyyy-MM-dd' (görünen ilk gün)",
191
+ end: "string 'yyyy-MM-dd' (görünen son gün)"
192
+ },
193
+ reads: "self'e yazılmaz — event.data ile oku; oklar, yatay kaydırma ve Bugün ile her görünümde"
194
+ },
195
+ viewChange: {
196
+ payload: { view: "'month' | 'week' | 'day'", start: "string 'yyyy-MM-dd'", end: "string 'yyyy-MM-dd'" },
197
+ reads: "self'e yazılmaz — event.data ile oku; görünüm düğmesiyle"
198
+ }
199
+ },
200
+ 'floating-container': {
201
+ mount: NO_DATA
82
202
  },
83
203
  repeater: {
84
204
  swipe: {
@@ -93,6 +213,13 @@ export const mobileComponentEventContracts = {
93
213
  index: 'number (satırın sırası, 0 tabanlı)',
94
214
  row: 'object (satırın verisi)'
95
215
  }
216
+ },
217
+ selectionChange: {
218
+ payload: {
219
+ selectedIndices: 'number[] — seçili satırların rows içindeki DÜZ indeksleri, sıralı',
220
+ count: 'number — seçili satır sayısı'
221
+ },
222
+ reads: 'yalnız selectionMode: multiple (ya da longPressSelects ile açılınca); akış event.data.selectedIndices / event.data.count okur, düğümün selectedIndices/selectedItems değeri de yazılır'
96
223
  }
97
224
  }
98
225
  };
@@ -1,7 +1,7 @@
1
1
  import { logStep } from '../observability/trace.js';
2
2
  import { PlatformApiError } from '../platform/errors.js';
3
3
  import { ensureWorkflowErrorPath } from './errorPath.js';
4
- import { TYPED_STYLE_COMPONENTS } from './styleResolve.js';
4
+ import { DOCUMENT_STYLE_TYPES, TYPED_STYLE_COMPONENTS } from './styleResolve.js';
5
5
  import { STEP_ALIASES } from './platform/profile.js';
6
6
  const SORT_DEFAULT = { name: 'sortField', type: 'sortType', source: 'query-string' };
7
7
  const LIMIT_DEFAULT = { name: 'limit', offset: 'offset', source: 'query-string' };
@@ -1886,12 +1886,22 @@ export function nestedRepeaterNodes(nodes) {
1886
1886
  walk(asObjects(nodes), false);
1887
1887
  return found;
1888
1888
  }
1889
- export function nestedRepeaterMessage(ids) {
1890
- return `PAGE-013 — repeater içinde repeater (istek gönderilmedi): ${ids.join(' · ')}. `
1889
+ /**
1890
+ * Mobil profilde gruplu listenin (gün/kategori başlığı altında satırlar) hazır bileşeni vardır:
1891
+ * `grouped-list` (ui-native, ADR 0007). Web'de karşılığı yok — web mesajı textarea tarifinde kalır.
1892
+ */
1893
+ export function nestedRepeaterMessage(ids, platform = 'web') {
1894
+ const measured = `PAGE-013 — repeater içinde repeater (istek gönderilmedi): ${ids.join(' · ')}. `
1891
1895
  + 'Ölçüldü (2026-08-19): iç repeater OKUMADA çalışır ama girilen değer dış satıra GERİ YAZILMAZ '
1892
1896
  + '(_repeaterValues null kalır) ve tek düzenlemeden sonra DOM bozulur. Sebep yapısal: şemada iç '
1893
- + 'repeater tek düğümdür, dış satır başına ayrı düğüm yoktur. Satır içi listeyi tek bir textarea '
1894
- + "ile topla (satır başına bir öğe) ve kaydederken split('\n') ile böl — platform_guide('page') Kural 1g.";
1897
+ + 'repeater tek düğümdür, dış satır başına ayrı düğüm yoktur. ';
1898
+ const textarea = "Satır içi listeyi tek bir textarea ile topla (satır başına bir öğe) ve kaydederken split('\n') ile böl — platform_guide('page') Kural 1g.";
1899
+ if (platform !== 'mobile')
1900
+ return measured + textarea;
1901
+ return measured
1902
+ + 'Başlıklar altında gruplanmış liste (güne/kategoriye göre) için mobilde tek `grouped-list` kur: düz `rows` + '
1903
+ + '`groupBy` alan yolu; çocuk 0 bölüm başlığı (`{{ Section.title }}`), çocuk 1 öğe şablonu (`Row`, `{{ item.<alan> }}`) — '
1904
+ + "platform_guide('mobile') §Güne göre görev listesi. Satır içinde düzenlenen alt liste gerekiyorsa: " + textarea;
1895
1905
  }
1896
1906
  /**
1897
1907
  * `style` prop'u düz CSS STRING verilmiş düğümler. Çalışır (emotion string'i CSS metni sayar) ama
@@ -1905,7 +1915,7 @@ export function stringStyleNodes(nodes) {
1905
1915
  for (const node of list) {
1906
1916
  const props = (node.props ?? {});
1907
1917
  const style = props.style;
1908
- if (style && typeof style.data === 'string' && style.data.includes(':')) {
1918
+ if (style && typeof style.data === 'string' && style.data.includes(':') && !DOCUMENT_STYLE_TYPES.has(String(node.type))) {
1909
1919
  found.push(String(node.elementId ?? node.name ?? '?'));
1910
1920
  }
1911
1921
  walk(asObjects(node.children));
@@ -1,5 +1,7 @@
1
- import { MOBILE_RULES, REDIRECT_PRESENTATIONS, missingComponentAdvice, styleReplacement } from './mobileRules.js';
1
+ import { IMAGE_SOURCE_PROPS, IMAGE_SOURCE_PROP_NAMES, MOBILE_RULES, REDIRECT_PRESENTATIONS, fileIdImageExpression, missingComponentAdvice, styleReplacement } from './mobileRules.js';
2
2
  import { findingLine } from './profile.js';
3
+ import { SURFACE_KEYS } from '../styleResolveNative.js';
4
+ import { DOCUMENT_STYLE_TYPES } from '../styleResolve.js';
3
5
  function asRecord(value) {
4
6
  return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
5
7
  }
@@ -18,10 +20,28 @@ function walk(value, visit) {
18
20
  walk(node.children, visit);
19
21
  }
20
22
  }
23
+ /**
24
+ * Koda ya da çeviriye bağlı prop: değeri çalışma anında `ref`'ten gelir, `data` yalnız tasarım yedeğidir.
25
+ * Belge CSS'i alan tipte (`DOCUMENT_STYLE_TYPES`) bağlı `style` metin sayılır — şema onu uzun metin ilan eder.
26
+ */
27
+ function boundWrapper(value) {
28
+ const wrapped = asRecord(value);
29
+ return wrapped !== undefined && (wrapped.type === 'code' || wrapped.type === 'translation');
30
+ }
21
31
  function propValue(value) {
22
32
  const wrapped = asRecord(value);
23
33
  return wrapped && 'data' in wrapped && typeof wrapped.type === 'string' ? wrapped.data : value;
24
34
  }
35
+ /** Prop'un bağlı ifadesi: code sarmalının `ref`i (yoksa `data`sı) ya da çıplak şablon metni. */
36
+ function boundExpression(value) {
37
+ if (typeof value === 'string')
38
+ return value;
39
+ const wrapped = asRecord(value);
40
+ if (!wrapped || wrapped.type !== 'code')
41
+ return undefined;
42
+ const expression = wrapped.ref ?? wrapped.data;
43
+ return typeof expression === 'string' ? expression : undefined;
44
+ }
25
45
  function label(node) {
26
46
  return String(node.elementId ?? node.name ?? node.type ?? '?');
27
47
  }
@@ -42,6 +62,10 @@ export function profileFindings(profile, input) {
42
62
  const unknownTypes = new Map();
43
63
  const shellNodes = new Map();
44
64
  const legacyNodes = [];
65
+ const fileIdSources = [];
66
+ const nodeTypes = new Map();
67
+ const floorImages = [];
68
+ const namelessIconButtons = [];
45
69
  const hit = (map, key, target) => map.set(key, [...(map.get(key) ?? []), target]);
46
70
  const inspectAppearance = (target, appearance) => {
47
71
  if (!appearance)
@@ -57,20 +81,45 @@ export function profileFindings(profile, input) {
57
81
  hit(styleHits, `${key} (${value})`, target);
58
82
  }
59
83
  };
84
+ // Mobil derleyici yüzey grubunun yalnız bilinen alanlarını okur; kalan alt anahtar cihazda düşer.
85
+ // `data`sız sarmal (`{ type: 'code', ref }`) bütün grubu bir ifadeye bağlar: denetlenecek alt anahtar yoktur.
86
+ const inspectSurface = (target, value) => {
87
+ const raw = asRecord(value);
88
+ if (raw && typeof raw.type === 'string' && !('data' in raw))
89
+ return;
90
+ for (const [key, wrapped] of Object.entries(asRecord(propValue(value)) ?? {})) {
91
+ const value = propValue(wrapped);
92
+ if (!SURFACE_KEYS.has(key) && value !== undefined && value !== null && value !== '')
93
+ hit(styleHits, `surfaceStyle.${key}`, target);
94
+ }
95
+ };
60
96
  const inspectNode = (node) => {
61
97
  const target = label(node);
62
98
  const type = String(node.type ?? '');
63
99
  const props = asRecord(node.props) ?? {};
64
100
  nodes.push(node);
101
+ nodeTypes.set(target, type);
102
+ for (const prop of IMAGE_SOURCE_PROPS[type] ?? []) {
103
+ const expression = fileIdImageExpression(boundExpression(props[prop]) ?? '');
104
+ if (expression)
105
+ fileIdSources.push({ target: `${target}.${prop}`, expression });
106
+ }
65
107
  if (Array.isArray(node.blocks) && node.blocks.length > 0 && !profile.layout.blocksMeaningful)
66
108
  blockNodes.push(target);
67
109
  const layout = propValue(props.layout);
68
110
  if (typeof layout === 'string' && GRID_LAYOUTS.has(layout))
69
111
  gridNodes.push(`${target} (layout:${layout})`);
70
112
  inspectAppearance(target, asRecord(node.appearance));
113
+ inspectSurface(target, props.surfaceStyle);
114
+ if (type === 'image' && !imageHasDefiniteHeight(asRecord(node.appearance)))
115
+ floorImages.push(target);
116
+ if (type === 'button' && propValue(props.size) === 'icon' && !String(propValue(props.ariaLabel) ?? '').trim() && !String(propValue(props.text) ?? '').trim()) {
117
+ namelessIconButtons.push(target);
118
+ }
71
119
  if (!profile.style.freeStyle) {
72
120
  const style = propValue(props.style);
73
- if ((asRecord(style) && Object.keys(style).length > 0) || (typeof style === 'string' && style.trim() !== ''))
121
+ const documentCss = DOCUMENT_STYLE_TYPES.has(type) && (typeof style === 'string' || boundWrapper(props.style));
122
+ if (!documentCss && ((asRecord(style) && Object.keys(style).length > 0) || (typeof style === 'string' && style.trim() !== '')))
74
123
  hit(styleHits, 'style', target);
75
124
  }
76
125
  if (!profile.style.hoverStyle) {
@@ -86,6 +135,8 @@ export function profileFindings(profile, input) {
86
135
  legacyNodes.push(`${target} (${type})`);
87
136
  };
88
137
  walk(input.nodes, inspectNode);
138
+ const savedTypes = new Map();
139
+ walk(input.savedNodes, node => savedTypes.set(label(node), String(node.type ?? '')));
89
140
  for (const op of input.ops ?? []) {
90
141
  const target = typeof op.target === 'string' ? op.target : '?';
91
142
  if (op.op === 'add_component')
@@ -97,12 +148,22 @@ export function profileFindings(profile, input) {
97
148
  if (op.op === 'replace_prop' || op.op === 'set_binding') {
98
149
  const prop = String(op.prop ?? '');
99
150
  const value = propValue(op.value);
151
+ const targetType = nodeTypes.get(target) ?? savedTypes.get(target) ?? '';
152
+ const documentCss = DOCUMENT_STYLE_TYPES.has(targetType) && (typeof value === 'string' || op.op === 'set_binding' || boundWrapper(op.value));
100
153
  if (prop === 'layout' && typeof value === 'string' && GRID_LAYOUTS.has(value))
101
154
  gridNodes.push(`${target} (layout:${value})`);
102
- if (prop === 'style' && !profile.style.freeStyle)
155
+ if (prop === 'style' && !profile.style.freeStyle && !documentCss)
103
156
  hit(styleHits, 'style', target);
104
157
  if (prop === 'hoverStyle' && !profile.style.hoverStyle)
105
158
  hit(styleHits, 'hoverStyle', target);
159
+ if (prop === 'surfaceStyle')
160
+ inspectSurface(target, op.value);
161
+ // MOB-011: var olan düğümün tipi op'ta yok — tip biliniyorsa (aynı yazımda eklendi) ona, yoksa prop adına bakılır.
162
+ const knownType = nodeTypes.get(target);
163
+ const imageProp = knownType === undefined ? IMAGE_SOURCE_PROP_NAMES.has(prop) : (IMAGE_SOURCE_PROPS[knownType] ?? []).includes(prop);
164
+ const expression = imageProp ? fileIdImageExpression(boundExpression(typeof op.expression === 'string' ? op.expression : op.value) ?? '') : undefined;
165
+ if (expression)
166
+ fileIdSources.push({ target: `${target}.${prop}`, expression });
106
167
  }
107
168
  }
108
169
  if (blockNodes.length > 0 || gridNodes.length > 0) {
@@ -141,6 +202,24 @@ export function profileFindings(profile, input) {
141
202
  guide: "examples/mobile-app-shell-{tab,stack,drawer}.json"
142
203
  });
143
204
  }
205
+ if (floorImages.length > 0) {
206
+ findings.push({
207
+ code: MOBILE_RULES['MOB-012'].id,
208
+ severity: 'info',
209
+ target: floorImages.join(', '),
210
+ message: "görselin boyu belirsiz, 96 dp taban boyla çizilir — boy verin (heightType: 'fixed' + heightString ya da 'fill': görsel kabuğu doldurur) ya da oran verin (appearance.aspectRatio + heightType: 'auto' + widthType 'fill'/'fixed')",
211
+ guide: "platform_guide('mobile')"
212
+ });
213
+ }
214
+ if (namelessIconButtons.length > 0) {
215
+ findings.push({
216
+ code: MOBILE_RULES['MOB-013'].id,
217
+ severity: 'info',
218
+ target: namelessIconButtons.join(', '),
219
+ message: "ikon boyundaki düğmenin adı yok — size: 'icon' metni çizmez ve ekran okuyucu ikonun ligatür adını (ör. 'notifications') okur; ariaLabel verin (ör. 'Bildirimler')",
220
+ guide: "platform_guide('mobile')"
221
+ });
222
+ }
144
223
  if (legacyNodes.length > 0) {
145
224
  findings.push({
146
225
  code: MOBILE_RULES['MOB-007'].id,
@@ -149,6 +228,17 @@ export function profileFindings(profile, input) {
149
228
  message: "sekme çubuğu ve çekmece menüsü katalog bileşeni değildir, çizer bu düğümleri yok sayar — sekmeler Ayarlar › Sekmeler'den (application_update properties.mobile.tabs), çekmece 'drawer' widget'ı + app-bar.menuTarget"
150
229
  });
151
230
  }
231
+ if (fileIdSources.length > 0) {
232
+ const sample = fileIdSources[0]?.expression ?? '';
233
+ const object = sample.replace(/\??\.value(\s*\}\})$/, '$1');
234
+ findings.push({
235
+ code: MOBILE_RULES['MOB-011'].id,
236
+ severity: 'warn',
237
+ target: fileIdSources.map(source => source.target).join(', '),
238
+ message: `${[...new Set(fileIdSources.map(source => `'${source.expression}'`))].join(', ')} '.value' ile bitiyor — kayıttan okunan FILE alanı { type: 'ref', value: <dosya id>, absolute_path: <url> } döner ve '.value' dosya KİMLİĞİDİR (GUID), görsel çizilmez. Nesnenin kendisini verin ('${object}') ya da '.absolute_path' yazın. Gateway alan tipini sayfa şemasından bilemez: yol FILE alanı değilse (ör. URL taşıyan metin alanı) uyarıyı yok sayın. Yazım platforma gitti.`,
239
+ guide: "platform_guide('mobile') §Dosya ve fotoğraf"
240
+ });
241
+ }
152
242
  const infoSteps = new Map();
153
243
  const badPresentation = new Map();
154
244
  walkSteps(input.steps, step => {
@@ -203,3 +293,20 @@ function walkSteps(value, visit) {
203
293
  export function findingLines(findings) {
204
294
  return findings.map(findingLine);
205
295
  }
296
+ /**
297
+ * Görselin boyu belli mi — mobil `imageFrameStyle` sırası: kabuktan (`fixed` + ölçülebilir `heightString`,
298
+ * ya da `fill`) ya da orandan (`aspectRatio` + `heightType: 'auto'` + genişlik `fill`/`fixed`+ölçü).
299
+ * Değilse görsel 96 dp taban boyla çizilir (MOB-012). Koşu bulgusu: `fixed`le kurulan görseller boşluk
300
+ * bıraktı, doğru kalıp yalnız bileşen açıklamasında yazıyordu (plans/mobil-plantcare-acigi g-10 adım 3).
301
+ */
302
+ function imageHasDefiniteHeight(appearance) {
303
+ if (!appearance)
304
+ return false;
305
+ // Çizicinin `cssLength`'i: sayı, `Npx` ya da `N%` — birimsiz dize ölçülemez.
306
+ const measurable = (value) => typeof value === 'number' || (typeof value === 'string' && /^\d+(\.\d+)?(px|%)$/.test(value.trim()));
307
+ const heightType = appearance.heightType;
308
+ if (heightType === 'fill' || (heightType === 'fixed' && measurable(appearance.heightString)))
309
+ return true;
310
+ const definiteWidth = appearance.widthType === 'fill' || (appearance.widthType === 'fixed' && measurable(appearance.widthString));
311
+ return Number(appearance.aspectRatio) > 0 && heightType === 'auto' && definiteWidth;
312
+ }
@@ -53,7 +53,7 @@ export const mobileProfile = Object.freeze({
53
53
  },
54
54
  style: { freeStyle: false, hoverStyle: false, resolver: 'native' },
55
55
  steps: {
56
- registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'navigate', 'storage', 'setcookie', 'download']),
56
+ registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'navigate', 'storage', 'setcookie', 'download', 'share']),
57
57
  aliases: STEP_ALIASES,
58
58
  infoOnly: new Set(['download', 'setcookie'])
59
59
  },
@@ -1,4 +1,5 @@
1
1
  import { MOBILE_COMPONENT_NOTES, mobileComponentKnowledge } from '../mobileComponentCatalog.js';
2
+ import { STYLE_GROUP_FIELDS } from '../styleResolve.js';
2
3
  export const MOBILE_RULES = {
3
4
  'MOB-001': {
4
5
  id: 'MOB-001',
@@ -10,7 +11,7 @@ export const MOBILE_RULES = {
10
11
  id: 'MOB-002',
11
12
  severity: 'warn',
12
13
  sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/style/compose-native-style.ts (yalnız tipli gruplar derlenir)',
13
- summary: 'Mobil sayfada CSS-only alan (className, tooltip, zIndex, serbest style, hoverStyle, em/rem/calc ölçü)'
14
+ summary: "Mobil sayfada CSS-only alan (className, tooltip, zIndex, serbest style, hoverStyle, em/rem/calc ölçü, surfaceStyle'ın tanımadığı alt anahtar)"
14
15
  },
15
16
  'MOB-003': {
16
17
  id: 'MOB-003',
@@ -33,8 +34,8 @@ export const MOBILE_RULES = {
33
34
  'MOB-006': {
34
35
  id: 'MOB-006',
35
36
  severity: 'warn',
36
- sourceRef: 'zeyrek-backend/docs/harici-componentler (protokol v1 tarayıcı .mjs; mobil v2 Faz 3)',
37
- summary: 'Harici bileşen mobil uygulamada desteklenmiyor — çağrı platforma gitmez'
37
+ sourceRef: 'zeyrek-backend/docs/harici-componentler (protokol v1 tarayıcı .mjs; mobil v2 Faz 3); plans/mobil-plantcare-acigi f-20 K7',
38
+ summary: "Harici bileşen mobil uygulamada desteklenmiyor — çağrı platforma gitmez; geçici yol 'html' bileşeni"
38
39
  },
39
40
  'MOB-007': {
40
41
  id: 'MOB-007',
@@ -47,8 +48,57 @@ export const MOBILE_RULES = {
47
48
  severity: 'warn',
48
49
  sourceRef: 'zeyrek-frontend/apps/mobile-runtime/src/navigation/navigation.ts (hrefFor); zeyrek-backend FlowDtoValidator',
49
50
  summary: "redirect.presentation kümede değil — push | modal | replace dışı değer backend'de 422, çizer push'a düşer"
51
+ },
52
+ 'MOB-011': {
53
+ id: 'MOB-011',
54
+ severity: 'warn',
55
+ sourceRef: 'zeyrek-backend FilePathMapper.MapFileProperties (okunan FILE alanı { type: ref, value: <dosya id>, absolute_path }); zeyrek-frontend ng-apps-ui-native image-logic.ts resolveImageSource (plans/mobil-kosu-acik-yuksekler f-01)',
56
+ summary: "Görsel kaynağı ifadesi '.value' ile bitiyor — kayıttan okunan FILE alanında bu dosya kimliğidir (GUID), görsel çizilmez"
57
+ },
58
+ 'MOB-012': {
59
+ id: 'MOB-012',
60
+ severity: 'info',
61
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/components/image/image-logic.ts (imageFrameStyle)',
62
+ summary: "Mobil görselin boyu ne kabuktan (fixed + ölçü, fill) ne orandan (aspectRatio + heightType auto + belli genişlik) geliyor — 96 dp taban boyla çizilir"
63
+ },
64
+ 'MOB-013': {
65
+ id: 'MOB-013',
66
+ severity: 'info',
67
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/components/button/index.tsx (accessibleLabel: ariaLabel → text → ikon adı)',
68
+ summary: "İkon boyunda (size: icon) düğmenin adı yok — ne ariaLabel ne text; ekran okuyucu ikonun ligatür adını (notifications) okur"
50
69
  }
51
70
  };
71
+ /**
72
+ * MOB-011: dosya nesnesini görsel olarak çizen prop'lar (mobil `resolveImageSource` tüketicileri).
73
+ * Hedefin tipi bilinmiyorsa (var olan düğüme `set_binding`) prop adı yeter — `IMAGE_SOURCE_PROP_NAMES`.
74
+ */
75
+ export const IMAGE_SOURCE_PROPS = {
76
+ image: ['src'],
77
+ avatar: ['src'],
78
+ 'list-item': ['leadingImage'],
79
+ card: ['image']
80
+ };
81
+ export const IMAGE_SOURCE_PROP_NAMES = new Set(Object.values(IMAGE_SOURCE_PROPS).flat());
82
+ const TEMPLATE = /\{\{([\s\S]*?)\}\}/g;
83
+ const MEMBER_PATH = /^[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*|\[\s*(?:\d+|'[^']*'|"[^"]*")\s*\])+$/;
84
+ /**
85
+ * MOB-011'in desen denetimi. Gateway sayfa şemasından bir alanın FILE olduğunu BİLEMEZ (alan tipi
86
+ * entity'de, ifade yalnız bir yol) — bu yüzden kural metinseldir ve uyarı düzeyindedir: en az üç
87
+ * parçalı bir üye yolu (`item.foto.value`, `pages.x.params.kayit.foto.value`) `.value` ile bitiyorsa
88
+ * ilk eşleşen `{{ }}` şablonunu döner. İki parçalı yol (`urlInput.value` — bileşenin kendi değeri) ve
89
+ * `.value.value` (dosya alanının base64'ü: `fileInput1.value.value`) eşleşmez.
90
+ */
91
+ export function fileIdImageExpression(template) {
92
+ for (const match of template.matchAll(TEMPLATE)) {
93
+ const body = (match[1] ?? '').trim();
94
+ if (!MEMBER_PATH.test(body))
95
+ continue;
96
+ const segments = body.match(/[A-Za-z_$][\w$]*|\[[^\]]*\]/g) ?? [];
97
+ if (segments.length >= 3 && segments[segments.length - 1] === 'value' && segments[segments.length - 2] !== 'value')
98
+ return match[0];
99
+ }
100
+ return undefined;
101
+ }
52
102
  /** `redirect` adımının tanıdığı sunum biçimleri (b-01 `StepArgument.Presentation`). */
53
103
  export const REDIRECT_PRESENTATIONS = new Set(['push', 'modal', 'replace']);
54
104
  /**
@@ -58,15 +108,19 @@ export const REDIRECT_PRESENTATIONS = new Set(['push', 'modal', 'replace']);
58
108
  * Mobil pakete giren bileşen Dalga 2/3 listesinden DÜŞER: yayında olduğunu mobil sözlükteki kaydı
59
109
  * söyler (`mobileComponentKnowledge`, parite testiyle `components.json`'a kilitli). Katalog A
60
110
  * (2026-09-24) sekiz web ikizini buradan indirdi: `radio`, `checkboxgroup`, `slider`, `tabs`, `badge`,
61
- * `avatar`, `progress`, `collapsible-container`; Katalog C (09) Dalga 3'ten `video` ve `signature`ı; dil dalgası
62
- * (plans/mobil-coklu-dil F1, 2026-09-25) Dalga 2'den `language-selector`ı. Dalga 1
111
+ * `avatar`, `progress`, `collapsible-container`; Katalog C (09) Dalga 3'ten `video`, `signature`, `chart`,
112
+ * `fileInput` ve `filebutton`ı, Dalga 2'den `datepicker` ve `timepicker`ı (`fileUpload`un sürükle-bırak alanı
113
+ * dokunmatikte yok: web'e özgü, karşılığı iki dosya alanı); dil dalgası (plans/mobil-coklu-dil F1, 2026-09-25)
114
+ * Dalga 2'den `language-selector`ı; faz 10b (2026-09-27) Dalga 3'ten `datatable`ı; plans/mobil-plantcare-acigi f-11
115
+ * (2026-09-27) listede olmayan `daterangepicker`i doğrudan pakete aldı; f-20 (2026-09-28) Dalga 3'ten `html`ı (mobilde
116
+ * geçici çözüm kapısı, K7); f-12 (2026-09-28) listede olmayan `eventcalendar`ı (web ikizi, ay/hafta/gün). Dalga 1
63
117
  * listesi artık yalnız `mobileNotes`'un bilgi satırıdır — MOB-003'te yayındaki bileşeni sözlük tanır
64
118
  * (`box-section` Dalga 1'de ama pakette yok).
65
119
  */
66
120
  export const MOBILE_COMPONENT_WAVES = Object.fromEntries([
67
121
  ...['container', 'box-section', 'form', 'slot', 'text', 'icon', 'image', 'divider', 'spacer', 'button', 'textinput', 'textarea', 'numberinput', 'password', 'checkbox', 'switch', 'dropdown', 'repeater', 'card', 'emptystate', 'form-validation-errors', 'modal', 'drawer', 'app-bar'].map(slug => [slug, 1]),
68
- ...['box', 'datepicker', 'timepicker', 'phoneinput', 'statcard', 'detailview', 'page-header', 'autocomplete'].map(slug => [slug, 2]),
69
- ...['datatable', 'chart', 'kanban', 'timeline', 'fileUpload', 'filebutton', 'fileInput', 'texteditor', 'html', 'stepper', 'tree-view'].map(slug => [slug, 3])
122
+ ...['box', 'phoneinput', 'statcard', 'detailview', 'page-header', 'autocomplete'].map(slug => [slug, 2]),
123
+ ...['kanban', 'timeline', 'texteditor', 'stepper', 'tree-view'].map(slug => [slug, 3])
70
124
  ]);
71
125
  export const MOBILE_WEB_ONLY = {
72
126
  iframe: "'webview' (src ya da html; bağlantılar varsayılan olarak sistem tarayıcısında açılır)",
@@ -79,17 +133,22 @@ export const MOBILE_WEB_ONLY = {
79
133
  popover: "'modal' widget'ı",
80
134
  'theme-toggle': 'cihaz teması otomatik (koyu/açık)',
81
135
  pagination: "'repeater' + daha fazla yükle düğmesi",
82
- breadcrumb: "yığın gezinmesi (app-bar geri düğmesi)"
136
+ breadcrumb: "yığın gezinmesi (app-bar geri düğmesi)",
137
+ fileUpload: "'fileInput' (alan) ya da 'filebutton' (düğme) — dokunmatikte sürükle-bırak yok; ikisi de aynı base64 değeri yazar"
83
138
  };
84
139
  const STYLE_REPLACEMENTS = {
85
140
  className: "yerine surfaceStyle (zemin, kenar, köşe, gölge) ve typographyStyle tipli gruplarını kullanın",
86
141
  tooltip: "dokunmatikte üzerine gelme yok — düğümü 'tooltip' bileşenine sarın (dokunuşla açılır) ya da açıklamayı metin olarak koyun",
87
142
  tooltipPosition: "dokunmatikte üzerine gelme yok — 'tooltip' bileşeninde balonun yanı `side` prop'udur",
88
- zIndex: "katman için 'modal' ya da 'drawer' widget'ı kullanın",
89
- style: "serbest CSS nesnesi React Native'de çizilmez — surfaceStyle ve typographyStyle tipli gruplarına taşıyın",
143
+ zIndex: "görselin üstüne rozet/hap/etiket/açıklama bindirmek için 'stack' (ilk çocuk taban, ötekiler üst katman; katman olarak fill×fill bir container koyup kendi hizasıyla köşeye yerleştirin), ekrana sabit içerik (FAB, alt eylem çubuğu, üst şerit) için 'floating-container' (anchor 9 nokta, offsetX/offsetY, fullWidth), kaydırırken üstte kalan başlık için scroll-view çocuğunda states.isSticky, sayfayı örten katman için 'modal' ya da 'drawer' widget'ı",
144
+ style: "serbest CSS nesnesi React Native'de çizilmez — surfaceStyle ve typographyStyle tipli gruplarına taşıyın; position/zIndex ile ekrana sabitlemek yerine 'floating-container' kullanın",
90
145
  hoverStyle: 'dokunmatikte üzerine gelme durumu yok'
91
146
  };
92
147
  export function styleReplacement(key) {
148
+ if (key.startsWith('surfaceStyle.')) {
149
+ const fields = (STYLE_GROUP_FIELDS.surfaceStyle ?? []).map(field => `\`${field}\``).join(', ');
150
+ return `yüzey grubunun alanları ${fields} (döndürme derece, ölçek yüzde — 100 aynı boy, kaydırma px)`;
151
+ }
93
152
  return STYLE_REPLACEMENTS[key] ?? 'mobil çizer bu alanı okumaz';
94
153
  }
95
154
  export function missingComponentAdvice(slug) {
@@ -115,7 +174,7 @@ export function mobileComponentNotes(slug) {
115
174
  const wave = slug === undefined ? undefined : MOBILE_COMPONENT_WAVES[slug];
116
175
  return [
117
176
  "Yerleşim yalnız flex: `layout` prop'u mobil şemalarda yoktur (grid/cssgrid yok), blocks/size yok sayılır, sıra dizi indeksidir; ölçü appearance ile verilir (widthType/heightType — MOB-001).",
118
- "Görünüm tipli gruplarla: surfaceStyle ve typographyStyle React Native stiline derlenir; ham CSS `style` prop'u hiçbir mobil şemada yoktur, className, `tooltip` alanı (ipucu için `tooltip` bileşeni), zIndex ve hoverStyle çizilmez (MOB-002). Karşılığı olmayan değer sessizce düşmez, uyarıya yazılır; düğümün gerçek stili: page_runtime_get(resolveStyles) → nativeStyle.",
177
+ "Görünüm tipli gruplarla: surfaceStyle ve typographyStyle React Native stiline derlenir; ham CSS `style` prop'u `html` dışında hiçbir mobil şemada yoktur (`html`'in metin `style`'ı kendi belgesine kapsamlı girer), className, `tooltip` alanı (ipucu için `tooltip` bileşeni), zIndex ve hoverStyle çizilmez (MOB-002). Karşılığı olmayan değer sessizce düşmez, uyarıya yazılır; düğümün gerçek stili: page_runtime_get(resolveStyles) → nativeStyle.",
119
178
  "Basılabilir bileşen role=\"button\" taşır; page_screenshot click onu kabuğun içinde bulur.",
120
179
  ...MOBILE_COMPONENT_NOTES,
121
180
  ...(wave === undefined ? [] : [`Mobil bileşen dalgası: ${wave} (plans/mobil-uygulama-expo §11).`])
@@ -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']),
23
+ registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share']),
24
24
  aliases: STEP_ALIASES,
25
25
  infoOnly: new Set()
26
26
  },