@appaflytech/zeyrek-ai-mcp 0.0.21 → 0.0.22

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 (51) hide show
  1. package/dist/config.js +2 -0
  2. package/dist/knowledge/componentCatalog.js +3 -3
  3. package/dist/knowledge/componentEvents.js +9 -2
  4. package/dist/knowledge/mobileComponentCatalog.js +543 -0
  5. package/dist/knowledge/mobileComponentEvents.js +98 -0
  6. package/dist/knowledge/normalize.js +52 -11
  7. package/dist/knowledge/platform/lint.js +205 -0
  8. package/dist/knowledge/platform/mobile.js +78 -0
  9. package/dist/knowledge/platform/mobileRules.js +123 -0
  10. package/dist/knowledge/platform/mobileTabs.js +108 -0
  11. package/dist/knowledge/platform/profile.js +20 -0
  12. package/dist/knowledge/platform/resolve.js +122 -0
  13. package/dist/knowledge/platform/web.js +38 -0
  14. package/dist/knowledge/rules.js +6 -8
  15. package/dist/knowledge/styleResolve.js +1 -1
  16. package/dist/knowledge/styleResolveNative.js +410 -0
  17. package/dist/knowledge/themeResolve.js +3 -3
  18. package/dist/platform/capture/actions.js +333 -0
  19. package/dist/platform/capture/browser.js +1 -0
  20. package/dist/platform/{screenshot.js → capture/capture.js} +23 -509
  21. package/dist/platform/capture/index.js +10 -0
  22. package/dist/platform/capture/readiness.js +103 -0
  23. package/dist/platform/capture/summary.js +77 -0
  24. package/dist/platform/capture/target.js +5 -0
  25. package/dist/platform/capture/targets/studio-mobile.js +15 -0
  26. package/dist/platform/capture/targets/studio-web.js +5 -0
  27. package/dist/platform/domSummary.js +1 -1
  28. package/dist/resources/examples/mobile-app-shell-drawer.json +209 -0
  29. package/dist/resources/examples/mobile-app-shell-stack.json +123 -0
  30. package/dist/resources/examples/mobile-app-shell-tab.json +174 -0
  31. package/dist/resources/examples/mobile-list-detail-form.json +656 -0
  32. package/dist/resources/guides/component.md +6 -1
  33. package/dist/resources/guides/functions.md +1 -1
  34. package/dist/resources/guides/layout.md +11 -7
  35. package/dist/resources/guides/mobile.md +264 -0
  36. package/dist/resources/guides/page.md +55 -15
  37. package/dist/resources/guides/translation.md +1 -1
  38. package/dist/server.js +2 -0
  39. package/dist/tools/applications.js +60 -3
  40. package/dist/tools/components.js +50 -10
  41. package/dist/tools/designSchemaPatch.js +13 -4
  42. package/dist/tools/externalComponents.js +25 -4
  43. package/dist/tools/flows.js +9 -2
  44. package/dist/tools/layouts.js +41 -9
  45. package/dist/tools/mobile.js +203 -0
  46. package/dist/tools/pages.js +115 -35
  47. package/dist/tools/platformInfo.js +2 -1
  48. package/dist/tools/profileWrite.js +45 -0
  49. package/dist/tools/screenWrite.js +2 -2
  50. package/dist/tools/screens.js +9 -3
  51. package/package.json +1 -1
@@ -0,0 +1,98 @@
1
+ /**
2
+ * MOBİLE ÖZGÜ olay yükleri. Mobil komponentlerin ÇOĞU web şemasını genişletir ve
3
+ * olayları web sözleşmesindedir (`componentEvents.ts`) — bu dosya yalnız web'de
4
+ * karşılığı OLMAYAN komponentler içindir (`app-bar` gibi kabuk komponentleri).
5
+ *
6
+ * Okuyucu önce buraya, sonra web sözlüğüne bakar; parity testi
7
+ * (`test/mobile-component-parity.test.ts`) her mobil olayın iki sözlükten
8
+ * birinde karşılanmasını zorunlu tutar.
9
+ */
10
+ const NO_DATA = { payload: {} };
11
+ export const mobileComponentEventContracts = {
12
+ 'choice-chips': {
13
+ change: {
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.'
16
+ }
17
+ }
18
+ },
19
+ tooltip: {
20
+ open: NO_DATA
21
+ },
22
+ calendar: {
23
+ mount: NO_DATA,
24
+ change: {
25
+ payload: {
26
+ value: "string 'yyyy-MM-ddT00:00:00' | null — seçilen gün (aralıkta başlangıç); web datepicker'ın biçimi: yerel duvar saati, gece yarısı, dilim eki yok",
27
+ endValue: "string 'yyyy-MM-ddT00:00:00' | null — YALNIZ mode: 'range'; ilk basışta null, ikinci basışta bitiş günü"
28
+ }
29
+ }
30
+ },
31
+ stack: {
32
+ mount: NO_DATA
33
+ },
34
+ 'list-item': {
35
+ click: NO_DATA
36
+ },
37
+ fab: {
38
+ click: NO_DATA
39
+ },
40
+ timer: {
41
+ mount: NO_DATA,
42
+ tick: { payload: { value: 'number — gösterilen saniye (geri sayımda kalan, kronometrede geçen); makine value prop\'una yazar' } },
43
+ finish: { payload: { isRunning: 'false — geri sayım 0\'a indi; makine isRunning\'i kapatır' } }
44
+ },
45
+ audio: {
46
+ mount: NO_DATA,
47
+ play: { payload: { isPlaying: "true — çalma başladı (denetim ya da akış); makine isPlaying'i yazar" } },
48
+ pause: { payload: { isPlaying: "false — çalma durdu (denetim, akış, bitiş ya da tarayıcının otomatik oynatma reddi); makine isPlaying'i yazar" } },
49
+ ended: { payload: { isPlaying: "false — ses sona vardı (loop kapalıyken, pause'tan sonra); makine isPlaying'i kapatır" } }
50
+ },
51
+ lottie: {
52
+ mount: NO_DATA,
53
+ finish: { payload: { isPlaying: "false — döngüsüz animasyon bitti; makine isPlaying'i kapatır, yeniden true yazmak baştan oynatır" } }
54
+ },
55
+ webview: {
56
+ mount: NO_DATA,
57
+ 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
+ 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)" } },
60
+ 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
+ },
62
+ markdown: {
63
+ mount: NO_DATA,
64
+ linkClick: {
65
+ payload: {
66
+ href: "string — dokunulan bağlantının ham adresi; şema süzülmeden gelir (dışarıda yalnız http(s), mailto, tel açılır)"
67
+ }
68
+ }
69
+ },
70
+ 'app-bar': {
71
+ clickBack: NO_DATA,
72
+ clickMenu: NO_DATA,
73
+ clickAction: { payload: { key: 'string (eylemin key alanı; yoksa dizideki sırası)' } }
74
+ },
75
+ 'safe-area': {
76
+ mount: NO_DATA
77
+ },
78
+ 'scroll-view': {
79
+ mount: NO_DATA,
80
+ scrollEnd: NO_DATA,
81
+ refresh: NO_DATA
82
+ },
83
+ repeater: {
84
+ swipe: {
85
+ payload: {
86
+ key: 'string (kaydırma eyleminin key alanı)',
87
+ index: 'number (satırın sırası, 0 tabanlı)',
88
+ row: 'object (satırın verisi)'
89
+ }
90
+ },
91
+ itemPress: {
92
+ payload: {
93
+ index: 'number (satırın sırası, 0 tabanlı)',
94
+ row: 'object (satırın verisi)'
95
+ }
96
+ }
97
+ }
98
+ };
@@ -2,6 +2,7 @@ import { logStep } from '../observability/trace.js';
2
2
  import { PlatformApiError } from '../platform/errors.js';
3
3
  import { ensureWorkflowErrorPath } from './errorPath.js';
4
4
  import { TYPED_STYLE_COMPONENTS } from './styleResolve.js';
5
+ import { STEP_ALIASES } from './platform/profile.js';
5
6
  const SORT_DEFAULT = { name: 'sortField', type: 'sortType', source: 'query-string' };
6
7
  const LIMIT_DEFAULT = { name: 'limit', offset: 'offset', source: 'query-string' };
7
8
  function asArray(value) {
@@ -269,12 +270,11 @@ export function normalizeWorkflowDefinition(definition) {
269
270
  */
270
271
  /**
271
272
  * Adım tipi sözlüğü backend'in, KOŞAN tipler barındırıcının: motor yedi tip register eder, web
272
- * `Runtime.svelte` altı, mobil `ScreenRuntime.tsx` beş tip ekler ve aynı sayfa kaydı ikisinde de
273
+ * `Runtime.svelte` altı, mobil `runtime-tasks.ts` yedi tip ekler ve aynı sayfa kaydı ikisinde de
273
274
  * koşar. Register edilmemiş bir tipe gelen akış sessizce ölür (`flow-engine.ts` `process()`
274
275
  * callback bulamazsa next/completed/stopped hiç çağrılmaz), üstelik `run()` promise'i
275
276
  * çözülmediği için `__ngAppsFlowActivity.running` sıfıra inmez. İki hâl tamamen türetilebilir
276
- * olduğu için lint değil NORMALİZASYON — kalanı (`confirm`/`download` mobilde yok) yalnız
277
- * uygulama tarafında kapanabilir, `FLOW-023` onları uyarır.
277
+ * olduğu için lint değil NORMALİZASYON; `FLOW-023` kalan hâlleri uyarır.
278
278
  */
279
279
  function normalizeFlowHostGaps(steps, list) {
280
280
  const fixes = [];
@@ -288,12 +288,15 @@ function normalizeFlowHostGaps(steps, list) {
288
288
  .map(step => String(step.stepId ?? ''))
289
289
  .filter(id => id.length > 0));
290
290
  for (const step of list) {
291
- // navigate ile redirect'in argümanı AYNI ({ path, pageId, query, targetBlank }) ama navigate'i
292
- // yalnız mobil register eder; redirect ise iki barındırıcıda da koşar (mobilde aynı navigate
293
- // uygulamasına eşlenir), yani redirect kesin üstün.
294
- if (step.type === 'navigate') {
295
- step.type = 'redirect';
296
- fixes.push(`'${label(step)}': type navigate → redirect yazıldı — argüman şekli aynı ({ path, pageId, query, targetBlank }) ama navigate'i web barındırıcısı register etmez, redirect ikisinde de koşar`);
291
+ // navigate ile redirect'in argümanı AYNI ({ path, pageId, query, targetBlank, presentation })
292
+ // ama navigate'i yalnız mobil register eder; redirect ise iki barındırıcıda da koşar (mobilde
293
+ // aynı navigate uygulamasına eşlenir), yani redirect kesin üstün. `presentation`
294
+ // (push | modal | replace) yalnız mobil barındırıcıda okunur; web yok sayar.
295
+ const alias = STEP_ALIASES[String(step.type ?? '')];
296
+ if (alias) {
297
+ const original = String(step.type);
298
+ step.type = alias;
299
+ fixes.push(`'${label(step)}': type ${original} → ${alias} yazıldı — argüman şekli aynı ({ path, pageId, query, targetBlank, presentation }) ama ${original}'i web barındırıcısı register etmez, ${alias} ikisinde de koşar`);
297
300
  }
298
301
  if (endStepIds.size === 0)
299
302
  continue;
@@ -650,6 +653,44 @@ export function styleKeyNote(fixed) {
650
653
  + 'değer kaydedilir ama hiç uygulanmaz ve tek işareti tarayıcı konsoludur (F-06). '
651
654
  + "Seçici anahtarlara ('&__body', ':hover', '--token') dokunulmadı."];
652
655
  }
656
+ /**
657
+ * Kabın `states.isDroppable`ı KATALOĞUN kararıdır. Gateway düğümü kurarken bu alanı
658
+ * yazmıyordu (`DEFAULT_STATES` hepsini `false` bırakır) ve Studio kanvası düğümü kap saymıyordu:
659
+ * imleç `scroll-view`un İÇİNDEYKEN bırakma planı sayfa kabına düşüyor, çizgi kabın üst kenarına
660
+ * çiziliyordu (ölçüldü 2026-09-21, `test/mobile app` › `Satislar`). Aynı tipi paletten ekleyen
661
+ * Studio katalog değerini yazdığı için tek şemada iki türlü davranan kap oluyordu.
662
+ *
663
+ * Değer uygulamanın KENDİ sözlüğünden gelir (`listComponents`; mobil uygulamada `type=mobile`
664
+ * satırları), yani kural platformdan bağımsızdır ve kuruluşun kendi komponentlerini de kapsar.
665
+ * Yalnız `true` yazılır: katalog bir tipi kap saymıyorsa düğüme dokunulmaz.
666
+ */
667
+ export function normalizeDroppableStates(nodes, droppable) {
668
+ const fixed = [];
669
+ if (droppable.size === 0)
670
+ return fixed;
671
+ for (const node of asObjects(nodes)) {
672
+ const type = typeof node.type === 'string' ? node.type : '';
673
+ if (droppable.get(type) === true) {
674
+ // `states` normalde `normalizeComponentDefaults`tan geçmiş olur; gelmediyse burada da
675
+ // eksiksiz kurulur — yarım `states` platformda NullReference üretir.
676
+ if (!node.states || typeof node.states !== 'object' || Array.isArray(node.states)) {
677
+ node.states = { ...DEFAULT_STATES };
678
+ }
679
+ const states = node.states;
680
+ if (states.isDroppable !== true) {
681
+ states.isDroppable = true;
682
+ fixed.push(`'${String(node.elementId ?? node.name ?? type)}' (${type})`);
683
+ }
684
+ }
685
+ fixed.push(...normalizeDroppableStates(node.children, droppable));
686
+ }
687
+ return fixed;
688
+ }
689
+ export function droppableStatesNote(fixed) {
690
+ return fixed.length === 0
691
+ ? []
692
+ : [`states.isDroppable katalogdan yazıldı (${fixed.join(' · ')}) — kataloğun kap saydığı düğüm Studio kanvasında da kap olmalı; alanı bilerek kapatmak için resize_component/set_states kullanın`];
693
+ }
653
694
  export function normalizeComponentDefaults(nodes) {
654
695
  const fixed = [];
655
696
  for (const node of asObjects(nodes)) {
@@ -884,11 +925,11 @@ export function pageContainerNode() {
884
925
  * çekilirlerse sayfa içeriğinin akışına girer ve katman olarak açılmazlar.
885
926
  */
886
927
  const ROOT_LAYER_TYPES = new Set(['modal', 'drawer']);
887
- export function ensurePageContainer(schema) {
928
+ export function ensurePageContainer(schema, rootContainer = pageContainerNode) {
888
929
  const nodes = asObjects(schema);
889
930
  if (nodes.some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID))
890
931
  return { schema: nodes, fixes: [] };
891
- const container = pageContainerNode();
932
+ const container = rootContainer();
892
933
  const layers = nodes.filter(node => ROOT_LAYER_TYPES.has(String(node.type ?? '')));
893
934
  container.children = nodes.filter(node => !ROOT_LAYER_TYPES.has(String(node.type ?? '')));
894
935
  const moved = container.children.length;
@@ -0,0 +1,205 @@
1
+ import { MOBILE_RULES, REDIRECT_PRESENTATIONS, missingComponentAdvice, styleReplacement } from './mobileRules.js';
2
+ import { findingLine } from './profile.js';
3
+ function asRecord(value) {
4
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
5
+ }
6
+ function walk(value, visit) {
7
+ if (!Array.isArray(value)) {
8
+ const single = asRecord(value);
9
+ if (single)
10
+ walk([single], visit);
11
+ return;
12
+ }
13
+ for (const entry of value) {
14
+ const node = asRecord(entry);
15
+ if (!node)
16
+ continue;
17
+ visit(node);
18
+ walk(node.children, visit);
19
+ }
20
+ }
21
+ function propValue(value) {
22
+ const wrapped = asRecord(value);
23
+ return wrapped && 'data' in wrapped && typeof wrapped.type === 'string' ? wrapped.data : value;
24
+ }
25
+ function label(node) {
26
+ return String(node.elementId ?? node.name ?? node.type ?? '?');
27
+ }
28
+ const GRID_LAYOUTS = new Set(['grid', 'cssgrid']);
29
+ const RELATIVE_UNIT = /\b\d*\.?\d+(em|rem)\b|calc\(/i;
30
+ /**
31
+ * Profilin taşınabilirlik bulguları. Web profilinde HER ZAMAN boş dizi döner — web davranışı
32
+ * değişmez (G1 kilidi); bulgular yalnız mobil profilde üretilir.
33
+ */
34
+ export function profileFindings(profile, input) {
35
+ if (profile.platform === 'web')
36
+ return [];
37
+ const findings = [];
38
+ const nodes = [];
39
+ const blockNodes = [];
40
+ const gridNodes = [];
41
+ const styleHits = new Map();
42
+ const unknownTypes = new Map();
43
+ const shellNodes = new Map();
44
+ const legacyNodes = [];
45
+ const hit = (map, key, target) => map.set(key, [...(map.get(key) ?? []), target]);
46
+ const inspectAppearance = (target, appearance) => {
47
+ if (!appearance)
48
+ return;
49
+ for (const key of profile.appearance.ignoredKeys) {
50
+ const value = appearance[key];
51
+ if (value !== undefined && value !== null && value !== '' && value !== 0)
52
+ hit(styleHits, key, target);
53
+ }
54
+ for (const key of ['widthString', 'heightString']) {
55
+ const value = appearance[key];
56
+ if (typeof value === 'string' && RELATIVE_UNIT.test(value))
57
+ hit(styleHits, `${key} (${value})`, target);
58
+ }
59
+ };
60
+ const inspectNode = (node) => {
61
+ const target = label(node);
62
+ const type = String(node.type ?? '');
63
+ const props = asRecord(node.props) ?? {};
64
+ nodes.push(node);
65
+ if (Array.isArray(node.blocks) && node.blocks.length > 0 && !profile.layout.blocksMeaningful)
66
+ blockNodes.push(target);
67
+ const layout = propValue(props.layout);
68
+ if (typeof layout === 'string' && GRID_LAYOUTS.has(layout))
69
+ gridNodes.push(`${target} (layout:${layout})`);
70
+ inspectAppearance(target, asRecord(node.appearance));
71
+ if (!profile.style.freeStyle) {
72
+ const style = propValue(props.style);
73
+ if ((asRecord(style) && Object.keys(style).length > 0) || (typeof style === 'string' && style.trim() !== ''))
74
+ hit(styleHits, 'style', target);
75
+ }
76
+ if (!profile.style.hoverStyle) {
77
+ const hover = propValue(props.hoverStyle);
78
+ if (asRecord(hover) && Object.keys(hover).length > 0)
79
+ hit(styleHits, 'hoverStyle', target);
80
+ }
81
+ if (type && input.catalog && input.catalog.size > 0 && !input.catalog.has(type) && !profile.shell.legacyShellNodes.has(type))
82
+ hit(unknownTypes, type, target);
83
+ if (type && profile.shell.suggestions[type])
84
+ hit(shellNodes, type, target);
85
+ if (type && profile.shell.legacyShellNodes.has(type))
86
+ legacyNodes.push(`${target} (${type})`);
87
+ };
88
+ walk(input.nodes, inspectNode);
89
+ for (const op of input.ops ?? []) {
90
+ const target = typeof op.target === 'string' ? op.target : '?';
91
+ if (op.op === 'add_component')
92
+ walk(op.component, inspectNode);
93
+ if (op.op === 'set_block' && !profile.layout.blocksMeaningful)
94
+ blockNodes.push(`${target} (set_block)`);
95
+ if (op.op === 'set_appearance')
96
+ inspectAppearance(target, asRecord(op.appearance));
97
+ if (op.op === 'replace_prop' || op.op === 'set_binding') {
98
+ const prop = String(op.prop ?? '');
99
+ const value = propValue(op.value);
100
+ if (prop === 'layout' && typeof value === 'string' && GRID_LAYOUTS.has(value))
101
+ gridNodes.push(`${target} (layout:${value})`);
102
+ if (prop === 'style' && !profile.style.freeStyle)
103
+ hit(styleHits, 'style', target);
104
+ if (prop === 'hoverStyle' && !profile.style.hoverStyle)
105
+ hit(styleHits, 'hoverStyle', target);
106
+ }
107
+ }
108
+ if (blockNodes.length > 0 || gridNodes.length > 0) {
109
+ findings.push({
110
+ code: MOBILE_RULES['MOB-001'].id,
111
+ severity: 'warn',
112
+ message: [
113
+ blockNodes.length > 0 ? `blok (12 sütun ızgara) mobil çizerde yok sayılır: ${[...new Set(blockNodes)].join(', ')}` : '',
114
+ gridNodes.length > 0 ? `grid/cssgrid yerleşimi yok, kap flex çizilir: ${gridNodes.join(', ')}` : ''
115
+ ].filter(Boolean).join(' · ') + ". Sıra dizi indeksinden, yerleşim flex'ten gelir (direction/gap/horizontalAlign). Yazım platforma gitti.",
116
+ guide: "platform_guide('mobile')"
117
+ });
118
+ }
119
+ for (const [key, targets] of styleHits) {
120
+ findings.push({
121
+ code: MOBILE_RULES['MOB-002'].id,
122
+ severity: 'warn',
123
+ target: [...new Set(targets)].join(', '),
124
+ message: `'${key}' mobilde çizilmez — ${styleReplacement(key.split(' ')[0] ?? key)}. Yazım platforma gitti.`
125
+ });
126
+ }
127
+ for (const [type, targets] of unknownTypes) {
128
+ findings.push({
129
+ code: MOBILE_RULES['MOB-003'].id,
130
+ severity: 'warn',
131
+ target: [...new Set(targets)].join(', '),
132
+ message: missingComponentAdvice(type)
133
+ });
134
+ }
135
+ for (const [type, targets] of shellNodes) {
136
+ findings.push({
137
+ code: MOBILE_RULES['MOB-004'].id,
138
+ severity: 'warn',
139
+ target: [...new Set(targets)].join(', '),
140
+ message: `'${type}' web kabuğudur, mobil uygulamada karşılığı: ${profile.shell.suggestions[type]}`,
141
+ guide: "examples/mobile-app-shell-{tab,stack,drawer}.json"
142
+ });
143
+ }
144
+ if (legacyNodes.length > 0) {
145
+ findings.push({
146
+ code: MOBILE_RULES['MOB-007'].id,
147
+ severity: 'warn',
148
+ target: legacyNodes.join(', '),
149
+ 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
+ });
151
+ }
152
+ const infoSteps = new Map();
153
+ const badPresentation = new Map();
154
+ walkSteps(input.steps, step => {
155
+ const type = String(step.type ?? '');
156
+ if (profile.steps.infoOnly.has(type))
157
+ hit(infoSteps, type, String(step.stepId ?? step.name ?? '?'));
158
+ // MOB-008: sunum biçimi yalnız `redirect`te anlamlı ve kümesi kapalı.
159
+ // Alan YOKSA bulgu yok — varsayılan `push` ve yazımların çoğu onu yazmaz.
160
+ if (type !== 'redirect')
161
+ return;
162
+ const args = asRecord(step.args);
163
+ const presentation = args?.presentation;
164
+ if (presentation === undefined || presentation === null || presentation === '')
165
+ return;
166
+ if (!REDIRECT_PRESENTATIONS.has(String(presentation))) {
167
+ hit(badPresentation, String(presentation), String(step.stepId ?? step.name ?? '?'));
168
+ }
169
+ });
170
+ for (const [value, stepIds] of badPresentation) {
171
+ findings.push({
172
+ code: MOBILE_RULES['MOB-008'].id,
173
+ severity: 'warn',
174
+ target: stepIds.join(', '),
175
+ message: `redirect.presentation '${value}' tanınmıyor — ${[...REDIRECT_PRESENTATIONS].join(' | ')} bekleniyor. Backend doğrulayıcısı 422 döner; geçse bile çizer 'push'a düşer.`,
176
+ guide: "platform_guide('mobile')"
177
+ });
178
+ }
179
+ for (const [type, stepIds] of infoSteps) {
180
+ findings.push({
181
+ code: MOBILE_RULES['MOB-005'].id,
182
+ severity: 'info',
183
+ target: stepIds.join(', '),
184
+ message: type === 'download'
185
+ ? "mobil uygulamada 'download' dosyayı cihazın paylaşım/açma sayfasında açar (Linking.openURL) — tarayıcı indirmesi yok"
186
+ : "mobil uygulamada 'setcookie' çerez yazmaz, cihaz deposuna (storage, expo-sqlite/kv-store) eşlenir — okuyan ifadeler aynı çalışır"
187
+ });
188
+ }
189
+ return findings;
190
+ }
191
+ function walkSteps(value, visit) {
192
+ if (!Array.isArray(value))
193
+ return;
194
+ for (const entry of value) {
195
+ const step = asRecord(entry);
196
+ if (!step)
197
+ continue;
198
+ visit(step);
199
+ walkSteps(step.steps, visit);
200
+ }
201
+ }
202
+ /** Bulguları `layoutWarnings[]` satırlarına çevirir. */
203
+ export function findingLines(findings) {
204
+ return findings.map(findingLine);
205
+ }
@@ -0,0 +1,78 @@
1
+ import { ENGINE_STEP_TYPES, STEP_ALIASES } from './profile.js';
2
+ import { PAGE_CONTAINER_ELEMENT_ID } from '../normalize.js';
3
+ /**
4
+ * Mobil uygulamanın kök kapsayıcısı. Web'in `pageContainerNode()`'undan üç farkı var:
5
+ *
6
+ * - `layout` prop'u YOK: mobil `container` şeması onu düşürür (`@ng-apps/ui-native`
7
+ * `components/container/schema.ts` `omitProps(['layout', …])`) — mobil kanvas yalnız flex akışıdır
8
+ * ve backend doğrulayıcısı mobil kataloğa bakar, yazılan `layout` bilinmeyen prop olurdu.
9
+ * - `direction: 'column'`, `wrap: false`: telefon ekranı dikey akar; web kökü satır + sarma ile
10
+ * 12 sütunu taklit eder.
11
+ * - `blocks: []` ve `heightType: 'fill'`: çizer blokları yok sayar (D3), kök ekranı doldurur.
12
+ */
13
+ export function mobileRootContainer() {
14
+ return {
15
+ elementId: PAGE_CONTAINER_ELEMENT_ID,
16
+ kind: 'component',
17
+ name: PAGE_CONTAINER_ELEMENT_ID,
18
+ type: 'container',
19
+ props: {
20
+ title: { type: 'static', data: '' },
21
+ direction: { type: 'static', data: 'column' },
22
+ horizontalAlign: { type: 'static', data: 'left' },
23
+ verticalAlign: { type: 'static', data: 'top' },
24
+ size: { type: 'static', data: 'grow' },
25
+ gap: { type: 'static', data: 'medium' },
26
+ wrap: { type: 'static', data: false }
27
+ },
28
+ blocks: [],
29
+ states: { isSticky: false, isDroppable: true, isScrollable: true, isWrapper: true },
30
+ appearance: { heightType: 'fill', marginValue: 0, marginType: 'none', paddingType: 'none', paddingValue: 0 },
31
+ events: [],
32
+ validations: [],
33
+ stateBindings: [],
34
+ children: []
35
+ };
36
+ }
37
+ /**
38
+ * Mobil profil = taşınabilir alt küme (`plans/mobil-uygulama-expo/01-mimari-tasarim.md` §5, §7).
39
+ * Yazım platforma YİNE gider (D3: şema aynı); alt kümenin dışı `MOB-*` bulgusu olarak uyarılır.
40
+ */
41
+ export const mobileProfile = Object.freeze({
42
+ platform: 'mobile',
43
+ catalogType: 'mobile',
44
+ layout: {
45
+ modes: new Set(['flex']),
46
+ blocksMeaningful: false,
47
+ rootContainer: mobileRootContainer,
48
+ orderBy: 'array-index'
49
+ },
50
+ appearance: {
51
+ ignoredKeys: new Set(['className', 'tooltip', 'tooltipPosition', 'zIndex']),
52
+ sizeUnits: new Set(['px', '%'])
53
+ },
54
+ style: { freeStyle: false, hoverStyle: false, resolver: 'native' },
55
+ steps: {
56
+ registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'navigate', 'storage', 'setcookie', 'download']),
57
+ aliases: STEP_ALIASES,
58
+ infoOnly: new Set(['download', 'setcookie'])
59
+ },
60
+ shell: {
61
+ components: new Set(['app-bar']),
62
+ suggestions: {
63
+ navigations: "sekmeler: application_update ile properties.mobile.tabs (Ayarlar › Sekmeler); üst çubuk için 'app-bar'",
64
+ sidebar: "çekmece: 'drawer' widget'ı + app-bar.leading:'menu' ve menuTarget",
65
+ header: "'app-bar' (başlık, leading: auto | none | back | menu, sağ eylemler)"
66
+ },
67
+ legacyShellNodes: new Set(['tab-bar', 'drawer-menu'])
68
+ },
69
+ externalComponents: false,
70
+ capture: {
71
+ target: 'studio-mobile',
72
+ devices: {
73
+ phone: { width: 390, height: 844, dpr: 2 },
74
+ tablet: { width: 820, height: 1180, dpr: 2 }
75
+ },
76
+ defaultDevice: 'phone'
77
+ }
78
+ });
@@ -0,0 +1,123 @@
1
+ import { MOBILE_COMPONENT_NOTES, mobileComponentKnowledge } from '../mobileComponentCatalog.js';
2
+ export const MOBILE_RULES = {
3
+ 'MOB-001': {
4
+ id: 'MOB-001',
5
+ severity: 'warn',
6
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/renderer (NativeRenderer blocks okumaz; sıra dizi indeksinden)',
7
+ summary: 'Mobil sayfada blok (12 sütun ızgara) ya da grid/cssgrid yerleşimi — çizer yok sayar'
8
+ },
9
+ 'MOB-002': {
10
+ id: 'MOB-002',
11
+ severity: 'warn',
12
+ 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
+ },
15
+ 'MOB-003': {
16
+ id: 'MOB-003',
17
+ severity: 'warn',
18
+ sourceRef: 'zeyrek-backend PageSchemaValidator unknown-type (mobil uygulama mobil kataloğa karşı doğrulanır)',
19
+ summary: 'Mobil katalogda olmayan bileşen tipi — backend 422 unknown-type ile reddeder'
20
+ },
21
+ 'MOB-004': {
22
+ id: 'MOB-004',
23
+ severity: 'warn',
24
+ sourceRef: 'plans/mobil-uygulama-expo/05-mobil-ayarlar-ve-gezinme.md §2 (D14)',
25
+ summary: "Mobil layout/sayfada web kabuğu (navigations, sidebar, header) — app-bar, sekmeler ayarı ve drawer widget'ı var"
26
+ },
27
+ 'MOB-005': {
28
+ id: 'MOB-005',
29
+ severity: 'info',
30
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/runtime/runtime-tasks.ts',
31
+ summary: "Mobil uygulamada download paylaşım/açma sayfası açar; setcookie storage'a eşlenir"
32
+ },
33
+ 'MOB-006': {
34
+ id: 'MOB-006',
35
+ 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'
38
+ },
39
+ 'MOB-007': {
40
+ id: 'MOB-007',
41
+ severity: 'warn',
42
+ sourceRef: 'plans/mobil-uygulama-expo/05-mobil-ayarlar-ve-gezinme.md §2.2, §2.6 (K13)',
43
+ summary: "Sekme ayarı tutarsız ya da layout/sayfada eski tab-bar/drawer-menu düğümü"
44
+ },
45
+ 'MOB-008': {
46
+ id: 'MOB-008',
47
+ severity: 'warn',
48
+ sourceRef: 'zeyrek-frontend/apps/mobile-runtime/src/navigation/navigation.ts (hrefFor); zeyrek-backend FlowDtoValidator',
49
+ summary: "redirect.presentation kümede değil — push | modal | replace dışı değer backend'de 422, çizer push'a düşer"
50
+ }
51
+ };
52
+ /** `redirect` adımının tanıdığı sunum biçimleri (b-01 `StepArgument.Presentation`). */
53
+ export const REDIRECT_PRESENTATIONS = new Set(['push', 'modal', 'replace']);
54
+ /**
55
+ * Bileşen dalgaları (`plans/mobil-uygulama-expo/01-mimari-tasarim.md` §11). MOB-003 bir web bileşeninin
56
+ * mobilde NE ZAMAN geleceğini söyler; `web-only` hiç gelmeyecekler ve karşılıkları.
57
+ *
58
+ * Mobil pakete giren bileşen Dalga 2/3 listesinden DÜŞER: yayında olduğunu mobil sözlükteki kaydı
59
+ * söyler (`mobileComponentKnowledge`, parite testiyle `components.json`'a kilitli). Katalog A
60
+ * (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
63
+ * 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
+ * (`box-section` Dalga 1'de ama pakette yok).
65
+ */
66
+ export const MOBILE_COMPONENT_WAVES = Object.fromEntries([
67
+ ...['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])
70
+ ]);
71
+ export const MOBILE_WEB_ONLY = {
72
+ iframe: "'webview' (src ya da html; bağlantılar varsayılan olarak sistem tarayıcısında açılır)",
73
+ sidebar: "'drawer' widget'ı + app-bar.menuTarget",
74
+ navigations: 'Ayarlar › Sekmeler (application_update properties.mobile.tabs)',
75
+ header: "'app-bar'",
76
+ 'layout-shell': "tek slot'lu sıradan layout + 'app-bar'",
77
+ 'layout-region': "tek slot'lu sıradan layout",
78
+ 'auth-layout': "tek slot'lu sıradan layout",
79
+ popover: "'modal' widget'ı",
80
+ 'theme-toggle': 'cihaz teması otomatik (koyu/açık)',
81
+ pagination: "'repeater' + daha fazla yükle düğmesi",
82
+ breadcrumb: "yığın gezinmesi (app-bar geri düğmesi)"
83
+ };
84
+ const STYLE_REPLACEMENTS = {
85
+ className: "yerine surfaceStyle (zemin, kenar, köşe, gölge) ve typographyStyle tipli gruplarını kullanın",
86
+ 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
+ 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",
90
+ hoverStyle: 'dokunmatikte üzerine gelme durumu yok'
91
+ };
92
+ export function styleReplacement(key) {
93
+ return STYLE_REPLACEMENTS[key] ?? 'mobil çizer bu alanı okumaz';
94
+ }
95
+ export function missingComponentAdvice(slug) {
96
+ // Yayındaki bileşen: katalog senkronu bu uygulamaya henüz yazmamış olabilir — "gelecek" demek yanlış olur.
97
+ if (Object.hasOwn(mobileComponentKnowledge, slug))
98
+ return `'${slug}' mobil pakette var ama bu uygulamanın kataloğunda henüz yok — components-sync mobil kataloğu yazdı mı (component_list)? Yazmadıysa backend 422 unknown-type döner`;
99
+ const wave = Object.hasOwn(MOBILE_COMPONENT_WAVES, slug) ? MOBILE_COMPONENT_WAVES[slug] : undefined;
100
+ if (wave === 2)
101
+ return `'${slug}' web'de var, mobilde Dalga 2 (F6 sonrası) — şimdilik Dalga 1 bileşenleriyle kurun (component_list)`;
102
+ if (wave === 3)
103
+ return `'${slug}' web'de var, mobilde Faz 3 (ayrı iş) — bu planda mobil karşılığı yok`;
104
+ const alternative = Object.hasOwn(MOBILE_WEB_ONLY, slug) ? MOBILE_WEB_ONLY[slug] : undefined;
105
+ if (alternative)
106
+ return `'${slug}' web'e özgüdür, mobilde gelmeyecek — ${alternative}`;
107
+ return `'${slug}' mobil katalogda yok — component_list ile mobil uygulamanın bileşenlerini görün`;
108
+ }
109
+ /**
110
+ * `component_get` mobil uygulamada (G4): kayıt mobil kataloğun satırıdır, davranış notları web
111
+ * runtime'ından (`knowledge`), bileşene özgü farklar mobil sözlükten (`mobileKnowledge`) gelir; bu
112
+ * liste BÜTÜN mobil bileşenlere geçen notlardır — kural kodlu üç not + `MOBILE_COMPONENT_NOTES`.
113
+ */
114
+ export function mobileComponentNotes(slug) {
115
+ const wave = slug === undefined ? undefined : MOBILE_COMPONENT_WAVES[slug];
116
+ return [
117
+ "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.",
119
+ "Basılabilir bileşen role=\"button\" taşır; page_screenshot click onu kabuğun içinde bulur.",
120
+ ...MOBILE_COMPONENT_NOTES,
121
+ ...(wave === undefined ? [] : [`Mobil bileşen dalgası: ${wave} (plans/mobil-uygulama-expo §11).`])
122
+ ];
123
+ }