@appaflytech/zeyrek-ai-mcp 0.0.41 → 0.0.42

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 (49) hide show
  1. package/dist/knowledge/componentCatalog.js +27 -97
  2. package/dist/knowledge/componentEvents.js +1 -18
  3. package/dist/knowledge/mobileComponentCatalog.js +114 -15
  4. package/dist/knowledge/mobileComponentEvents.js +41 -0
  5. package/dist/knowledge/motionResolve.js +252 -0
  6. package/dist/knowledge/normalize.js +87 -59
  7. package/dist/knowledge/pageDiscover.js +2 -2
  8. package/dist/knowledge/platform/batchOps.js +70 -0
  9. package/dist/knowledge/platform/heroTag.js +153 -0
  10. package/dist/knowledge/platform/lint.js +128 -6
  11. package/dist/knowledge/platform/mobile.js +1 -1
  12. package/dist/knowledge/platform/mobileLayoutLint.js +8 -1
  13. package/dist/knowledge/platform/mobileRules.js +18 -0
  14. package/dist/knowledge/platform/pageTransition.js +57 -0
  15. package/dist/knowledge/platform/web.js +1 -1
  16. package/dist/knowledge/rules.js +52 -1
  17. package/dist/knowledge/styleResolveNative.js +88 -4
  18. package/dist/platform/capture/actions.js +4 -1
  19. package/dist/platform/capture/capture.js +10 -13
  20. package/dist/platform/capture/persistent-browser.js +130 -0
  21. package/dist/resources/examples/flow-list-to-form.json +8 -10
  22. package/dist/resources/examples/flow-load-table.json +1 -1
  23. package/dist/resources/examples/mobile-list-detail-form.json +4 -3
  24. package/dist/resources/examples/mobile-scan-code.json +285 -0
  25. package/dist/resources/guides/component.md +3 -3
  26. package/dist/resources/guides/expressions.md +2 -2
  27. package/dist/resources/guides/merge.md +98 -18
  28. package/dist/resources/guides/microservice.md +2 -2
  29. package/dist/resources/guides/mobile-layout.md +2 -1
  30. package/dist/resources/guides/mobile-publish.md +1 -0
  31. package/dist/resources/guides/mobile-steps.md +67 -0
  32. package/dist/resources/guides/mobile.md +27 -55
  33. package/dist/resources/guides/page.md +182 -121
  34. package/dist/resources/guides/translation.md +26 -0
  35. package/dist/resources/schema-fingerprint.json +2 -2
  36. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +30 -1
  37. package/dist/tools/activities.js +3 -1
  38. package/dist/tools/applications.js +16 -2
  39. package/dist/tools/components.js +12 -6
  40. package/dist/tools/designSchemaPatch.js +18 -13
  41. package/dist/tools/layouts.js +7 -2
  42. package/dist/tools/merges.js +29 -9
  43. package/dist/tools/pages.js +129 -127
  44. package/dist/tools/platformInfo.js +3 -2
  45. package/dist/tools/profileWrite.js +2 -2
  46. package/dist/tools/schemaOps.js +132 -34
  47. package/dist/tools/screenWrite.js +1 -1
  48. package/dist/tools/screens.js +3 -3
  49. package/package.json +2 -2
@@ -1,9 +1,19 @@
1
1
  import { IMAGE_SOURCE_PROPS, IMAGE_SOURCE_PROP_NAMES, MOBILE_RULES, REDIRECT_PRESENTATIONS, fileIdImageExpression, missingComponentAdvice, styleReplacement } from './mobileRules.js';
2
+ import { flattenBatchOps } from './batchOps.js';
3
+ import { heroTagProblems, heroTagsAfterWrite, writesHeroTag } from './heroTag.js';
4
+ import { PAGE_TRANSITIONS, resolveTransition } from './pageTransition.js';
2
5
  import { findingLine } from './profile.js';
3
6
  import { SURFACE_KEYS } from '../styleResolveNative.js';
7
+ import { MOTION_BINDING_TYPES, MOTION_FIELDS, motionFieldBinding, motionValue } from '../motionResolve.js';
4
8
  import { DOCUMENT_STYLE_TYPES } from '../styleResolve.js';
5
9
  import { listThemeVarNames } from '../themeResolve.js';
6
10
  import { closestArgument } from '../../platform/argumentAliases.js';
11
+ /**
12
+ * Uyarının yazımı durdurmadığını söyleyen cümle. OLAYI değil KURALI anlatır: aynı bulgu kuru koşuda
13
+ * (`page_schema_validate`) da döner ve orada hiçbir şey yazılmamıştır — "Yazım platforma gitti" kuru koşuda yanlıştı
14
+ * (TOOL-060, ölçüldü 2026-10-10, gerçek uç). Regresyon: `test/dry-run-wording.test.ts`.
15
+ */
16
+ const NOT_BLOCKING = 'Yazım reddedilmez, platforma gider.';
7
17
  function asRecord(value) {
8
18
  return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
9
19
  }
@@ -49,6 +59,7 @@ function label(node) {
49
59
  }
50
60
  const GRID_LAYOUTS = new Set(['grid', 'cssgrid']);
51
61
  const RELATIVE_UNIT = /\b\d*\.?\d+(em|rem)\b|calc\(/i;
62
+ const TEMPLATE_VALUE = /\{\{[\s\S]*\}\}/;
52
63
  /**
53
64
  * THEME-012: mobil çizici `var(--ng-…)`'yi yalnız temanın yaydığı değişkenlerden çözer
54
65
  * (`compose-native-style.ts` `resolveThemeToken`); web'deki bileşen yerel değişkenleri (`--ng-nav-bg` …)
@@ -103,6 +114,7 @@ export function profileFindings(profile, input) {
103
114
  const floorImages = [];
104
115
  const namelessIconButtons = [];
105
116
  const unknownTokens = [];
117
+ const motionSkips = [];
106
118
  const hit = (map, key, target) => map.set(key, [...(map.get(key) ?? []), target]);
107
119
  const inspectTokens = (target, value) => {
108
120
  for (const name of unknownThemeTokens(value)) {
@@ -136,6 +148,42 @@ export function profileFindings(profile, input) {
136
148
  hit(styleHits, `surfaceStyle.${key}`, target);
137
149
  }
138
150
  };
151
+ // MOB-028: hareket çözücüsü tanımadığı alt anahtarı ve okuyamadığı değeri ATLAR, nesne olmayan grubu hareket
152
+ // saymaz (frontend `resolve-motion.ts`) — yazım kabul edilir, hareket uyarısız eksik çıkar. Uyarı metni
153
+ // çözücününkidir. İfadeye bağlı GRUP (`code`/`translation` sarmalı; `data`sı yalnız tasarım yedeğidir) çalışma
154
+ // anında çözülür: burada okunamaz, denetlenmez. Grubun içindeki TEK alan ifadeye (`code`) bağlıysa o da çalışma
155
+ // anında değerlendirilir (frontend ADR 0020): uyarı değildir, yalnız tasarım yedeği denetlenir. Alanda çözülmeyen
156
+ // yazım (şablon metni, `code` dışı bağ) söylenir (`motionFieldBinding`).
157
+ const inspectMotion = (target, value) => {
158
+ if (boundWrapper(value))
159
+ return;
160
+ const raw = asRecord(value);
161
+ // Veri taşımayan sarmal yalnız BİLİNEN bir değer türüyse sarmaldır: alanı yanlışlıkla `type` diye yazılmış ham
162
+ // grup (`{ type: 'fade', duration: 300 }`) sarmal sanılıp denetimsiz geçiyordu.
163
+ if (raw && typeof raw.type === 'string' && (raw.type === 'static' || MOTION_BINDING_TYPES.includes(raw.type)) && !('data' in raw))
164
+ return;
165
+ const group = propValue(value);
166
+ if (group === undefined || group === null || group === '' || (typeof group === 'string' && TEMPLATE_VALUE.test(group)))
167
+ return;
168
+ const fields = asRecord(group);
169
+ if (!fields) {
170
+ motionSkips.push(`${target}: motionStyle: ${typeof group === 'string' ? `'${group}'` : JSON.stringify(group)} nesne değil — alanlar nesnenin içinde yazılır ({ enter: 'fade' }), hareket yok sayılır`);
171
+ return;
172
+ }
173
+ for (const [key, wrapped] of Object.entries(fields)) {
174
+ const binding = motionFieldBinding(key, wrapped);
175
+ if (binding?.kind === 'unresolved')
176
+ motionSkips.push(`${target}: ${binding.warning}`);
177
+ const field = binding ? binding.fallback : propValue(wrapped);
178
+ if (field === undefined || field === null || field === '')
179
+ continue;
180
+ const warning = MOTION_FIELDS.includes(key)
181
+ ? motionValue(key, field).warning
182
+ : `motionStyle.${key}: tanınmayan alan, atlandı`;
183
+ if (warning)
184
+ motionSkips.push(`${target}: ${warning}`);
185
+ }
186
+ };
139
187
  const inspectNode = (node) => {
140
188
  const target = label(node);
141
189
  const type = String(node.type ?? '');
@@ -154,6 +202,7 @@ export function profileFindings(profile, input) {
154
202
  gridNodes.push(`${target} (layout:${layout})`);
155
203
  inspectAppearance(target, asRecord(node.appearance));
156
204
  inspectSurface(target, props.surfaceStyle);
205
+ inspectMotion(target, props.motionStyle);
157
206
  for (const group of STYLE_GROUP_PROPS)
158
207
  inspectTokens(target, props[group]);
159
208
  if (type === 'image' && !imageHasDefiniteHeight(asRecord(node.appearance)))
@@ -182,7 +231,9 @@ export function profileFindings(profile, input) {
182
231
  walk(input.nodes, inspectNode);
183
232
  const savedTypes = new Map();
184
233
  walk(input.savedNodes, node => savedTypes.set(label(node), String(node.type ?? '')));
185
- for (const op of input.ops ?? []) {
234
+ // Toplu yamanın zarfı açılır: içindeki op'lar tek tek yazılmış gibi denetlenir (TOOL-057).
235
+ const ops = flattenBatchOps(input.ops ?? []);
236
+ for (const op of ops) {
186
237
  const target = typeof op.target === 'string' ? op.target : '?';
187
238
  if (op.op === 'add_component')
188
239
  walk(op.component, inspectNode);
@@ -203,6 +254,8 @@ export function profileFindings(profile, input) {
203
254
  hit(styleHits, 'hoverStyle', target);
204
255
  if (prop === 'surfaceStyle')
205
256
  inspectSurface(target, op.value);
257
+ if (prop === 'motionStyle')
258
+ inspectMotion(target, op.value);
206
259
  if (STYLE_GROUP_PROPS.has(prop))
207
260
  inspectTokens(target, op.value);
208
261
  // 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.
@@ -220,7 +273,7 @@ export function profileFindings(profile, input) {
220
273
  message: [
221
274
  blockNodes.length > 0 ? `blok (12 sütun ızgara) mobil çizerde yok sayılır: ${[...new Set(blockNodes)].join(', ')}` : '',
222
275
  gridNodes.length > 0 ? `grid/cssgrid yerleşimi yok, kap flex çizilir: ${gridNodes.join(', ')}` : ''
223
- ].filter(Boolean).join(' · ') + ". Sıra dizi indeksinden, yerleşim flex'ten gelir (direction/gap/horizontalAlign). Yazım platforma gitti.",
276
+ ].filter(Boolean).join(' · ') + ". Sıra dizi indeksinden, yerleşim flex'ten gelir (direction/gap/horizontalAlign). " + NOT_BLOCKING,
224
277
  guide: "platform_guide('mobile')"
225
278
  });
226
279
  }
@@ -229,7 +282,33 @@ export function profileFindings(profile, input) {
229
282
  code: MOBILE_RULES['MOB-002'].id,
230
283
  severity: 'warn',
231
284
  target: [...new Set(targets)].join(', '),
232
- message: `'${key}' mobilde çizilmez — ${styleReplacement(key.split(' ')[0] ?? key)}. Yazım platforma gitti.`
285
+ message: `'${key}' mobilde çizilmez — ${styleReplacement(key.split(' ')[0] ?? key)}. ${NOT_BLOCKING}`
286
+ });
287
+ }
288
+ // MOB-030: aynı sayfada aynı statik heroTag (frontend `hero-tag.ts`; ayna heroTag.ts). Sayfanın yazımdan SONRAKİ
289
+ // etiketleri okunur ve yalnız bu yazımın dokunduğu bir düğümü içeren yineleme söylenir — kayıtlı ağaçta zaten duran
290
+ // yineleme ilgisiz her yazımda yinelenmez. Bilgidir: yineleme yalnız sayfa bir yakınlaşmanın HEDEFİ olduğunda
291
+ // belirsizdir; kaynak sayfada aynı etiketli kartlar olağandır (kaynak dokunulan düğümdür).
292
+ const hero = heroTagsAfterWrite({ nodes: input.nodes, savedNodes: input.savedNodes, ops });
293
+ const heroDuplicates = [...new Set(hero.entries.filter(entry => entry.tag !== undefined).map(entry => entry.tag))]
294
+ .map(tag => hero.entries.filter(entry => entry.tag === tag))
295
+ .filter(group => group.length > 1 && group.some(entry => hero.written.has(entry.elementId)));
296
+ if (heroDuplicates.length > 0) {
297
+ findings.push({
298
+ code: MOBILE_RULES['MOB-030'].id,
299
+ severity: 'info',
300
+ target: heroDuplicates.flatMap(group => group.map(entry => entry.elementId)).join(', '),
301
+ message: `${heroDuplicates.flatMap(group => heroTagProblems(group)).join(' · ')}. Yalnız bu sayfa bir yakınlaşmanın HEDEFİ olduğunda önemlidir: hizalama kutusu tektir. Kaynak sayfada aynı etiketli kartlar ve liste satırları sorun değildir (kaynak dokunulan düğümdür); hedefte ayrı öğelerse ayrı etiket verin.`,
302
+ guide: "platform_guide('page') §Kural 1g-3"
303
+ });
304
+ }
305
+ if (motionSkips.length > 0) {
306
+ findings.push({
307
+ code: MOBILE_RULES['MOB-028'].id,
308
+ severity: 'warn',
309
+ target: [...new Set(motionSkips.map(entry => entry.split(':')[0] ?? entry))].join(', '),
310
+ message: `motionStyle'da çözücünün atladığı alan var, hareket eksik çıkar: ${[...new Set(motionSkips)].join(' · ')}. Alanlar: ${MOTION_FIELDS.join(', ')}. ${NOT_BLOCKING}`,
311
+ guide: "platform_guide('mobile')"
233
312
  });
234
313
  }
235
314
  if (unknownTokens.length > 0) {
@@ -237,7 +316,7 @@ export function profileFindings(profile, input) {
237
316
  code: 'THEME-012',
238
317
  severity: 'warn',
239
318
  target: [...new Set(unknownTokens.map(entry => entry.split(':')[0] ?? entry))].join(', '),
240
- message: `temada tanımlı olmayan jeton cihazda çözülmez (zemin saydam kalır, iOS gölgesi içeriğe düşer): ${[...new Set(unknownTokens)].join(' · ')}. Geçerli adlar temanın yaydığı değişkenlerdir (theme_get); bilerek kullanıyorsanız yedek verin: var(--ad, #fff). Yazım platforma gitti.`,
319
+ message: `temada tanımlı olmayan jeton cihazda çözülmez (zemin saydam kalır, iOS gölgesi içeriğe düşer): ${[...new Set(unknownTokens)].join(' · ')}. Geçerli adlar temanın yaydığı değişkenlerdir (theme_get); bilerek kullanıyorsanız yedek verin: var(--ad, #fff). ${NOT_BLOCKING}`,
241
320
  guide: "platform_guide('theme')"
242
321
  });
243
322
  }
@@ -291,12 +370,14 @@ export function profileFindings(profile, input) {
291
370
  code: MOBILE_RULES['MOB-011'].id,
292
371
  severity: 'warn',
293
372
  target: fileIdSources.map(source => source.target).join(', '),
294
- 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.`,
373
+ 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. ${NOT_BLOCKING}`,
295
374
  guide: "platform_guide('mobile') §Dosya ve fotoğraf"
296
375
  });
297
376
  }
298
377
  const infoSteps = new Map();
299
378
  const badPresentation = new Map();
379
+ const badTransition = new Map();
380
+ const unusedTransition = new Map();
300
381
  walkSteps(input.steps, step => {
301
382
  const type = String(step.type ?? '');
302
383
  if (profile.steps.infoOnly.has(type))
@@ -307,10 +388,24 @@ export function profileFindings(profile, input) {
307
388
  return;
308
389
  const args = asRecord(step.args);
309
390
  const presentation = args?.presentation;
391
+ const stepId = String(step.stepId ?? step.name ?? '?');
392
+ // MOB-029: geçişin kararını çalışma zamanının çözücüsü verir (ayna: pageTransition.ts) — lint ikinci bir kural
393
+ // yazmaz. Alan YOKSA bulgu yok: adım uygulamanın varsayılanını kullanır. Sekme hedefi burada bilinmez
394
+ // (lint sekme ayarını görmez); o durumda da geçiş kullanılmaz ve rehber bunu söyler.
395
+ // Akış araçları bu lint'i BAŞARILI yazımdan sonra koşar ve küme dışı değeri backend 422 ile önce reddeder: uyarı
396
+ // dalı bugün yalnız backend'in doğrulamadığı bir yazım yolunda konuşur (MOB-008 ile aynı durum); bilgi dalı canlıdır.
397
+ if (args?.transition !== undefined && args.transition !== null && args.transition !== '') {
398
+ const resolved = resolveTransition({ step: args.transition, presentation }, { reduceMotion: false });
399
+ const written = typeof args.transition === 'string' ? args.transition : JSON.stringify(args.transition);
400
+ if (resolved.warnings.length > 0)
401
+ hit(badTransition, written, stepId);
402
+ else if (resolved.fixedBy === 'modal')
403
+ hit(unusedTransition, written, stepId);
404
+ }
310
405
  if (presentation === undefined || presentation === null || presentation === '')
311
406
  return;
312
407
  if (!REDIRECT_PRESENTATIONS.has(String(presentation))) {
313
- hit(badPresentation, String(presentation), String(step.stepId ?? step.name ?? '?'));
408
+ hit(badPresentation, String(presentation), stepId);
314
409
  }
315
410
  });
316
411
  for (const [value, stepIds] of badPresentation) {
@@ -322,6 +417,24 @@ export function profileFindings(profile, input) {
322
417
  guide: "platform_guide('mobile')"
323
418
  });
324
419
  }
420
+ for (const [value, stepIds] of badTransition) {
421
+ findings.push({
422
+ code: MOBILE_RULES['MOB-029'].id,
423
+ severity: 'warn',
424
+ target: stepIds.join(', '),
425
+ message: `redirect.transition '${value}' tanınmıyor — ${PAGE_TRANSITIONS.join(', ')} bekleniyor. Backend doğrulayıcısı 422 döner; alanı yazmazsanız adım uygulamanın varsayılan geçişiyle açılır.`,
426
+ guide: "platform_guide('page') §Kural 1g-2"
427
+ });
428
+ }
429
+ for (const [value, stepIds] of unusedTransition) {
430
+ findings.push({
431
+ code: MOBILE_RULES['MOB-029'].id,
432
+ severity: 'info',
433
+ target: stepIds.join(', '),
434
+ message: `redirect.transition '${value}' modal sunumla birlikte yazılmış — modal sistemin kağıt animasyonuyla açılır, geçiş kullanılmaz (hedef bir sekme sayfasıysa da kullanılmaz: sekme değişir).`,
435
+ guide: "platform_guide('page') §Kural 1g-2"
436
+ });
437
+ }
325
438
  for (const [type, stepIds] of infoSteps) {
326
439
  findings.push({
327
440
  code: MOBILE_RULES['MOB-005'].id,
@@ -345,6 +458,15 @@ function walkSteps(value, visit) {
345
458
  walkSteps(step.steps, visit);
346
459
  }
347
460
  }
461
+ /**
462
+ * Denetim bu yama için kayıtlı ağacı ister mi (`ProfileLintInput.savedNodes`): `style` yazımında hedefin tipi (belge
463
+ * CSS'i taşıyan düğüm mü — MOB-002), `heroTag` yazımında sayfanın öteki etiketleri (MOB-030). Çağıran ağacı yalnız bu
464
+ * durumda okur. Denetimle AYNI op listesine bakar (zarf açılır): 2026-10-10'da denetim zarfı açıp çağıran açmayınca
465
+ * kayıtlı `html` düğümünün belge CSS'i zarf içinde yanlış MOB-002 aldı (regresyon `test/batch-profile-lint.test.ts`).
466
+ */
467
+ export function profileLintReadsSavedNodes(ops) {
468
+ return writesHeroTag(ops) || flattenBatchOps(ops).some(op => (op.op === 'replace_prop' || op.op === 'set_binding') && op.prop === 'style');
469
+ }
348
470
  /** Bulguları `layoutWarnings[]` satırlarına çevirir. */
349
471
  export function findingLines(findings) {
350
472
  return findings.map(findingLine);
@@ -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', 'ad-show', 'back']),
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', 'back', 'scan-code']),
120
120
  aliases: STEP_ALIASES,
121
121
  infoOnly: new Set(['download', 'setcookie'])
122
122
  },
@@ -1,4 +1,4 @@
1
- import { MOBILE_DROPPABLE_TYPES, MOBILE_OUT_OF_FLOW_TYPES, MOBILE_TRANSPARENT_WRAPPER_TYPES } from '../mobileComponentCatalog.js';
1
+ import { MOBILE_DROPPABLE_TYPES, MOBILE_OPTIONAL_VIEW_INDEX, MOBILE_OUT_OF_FLOW_TYPES, MOBILE_TRANSPARENT_WRAPPER_TYPES } from '../mobileComponentCatalog.js';
2
2
  import { PAGE_CONTAINER_ELEMENT_ID } from '../normalize.js';
3
3
  import { OVERLAY_TYPES, firstDrawn, safeAreaEdges, topSafeAreaMissing } from './mobile.js';
4
4
  import { MOBILE_RULES } from './mobileRules.js';
@@ -82,6 +82,8 @@ export function mobileLayoutFindings(input) {
82
82
  return;
83
83
  if (type === 'safe-area' && ancestors.length === 0)
84
84
  return;
85
+ if (isOptionalView(node, ancestors.at(-1)))
86
+ return;
85
87
  findings.push({ ...rule('MOB-018', label(node), `boş ${type}: ekranda boş şerit bırakır${type === 'safe-area' ? ` (edges: '${safeAreaEdges(node)}' payı kadar)` : ''}. İçeriğini ekleyin ya da silin`), severity: 'info' });
86
88
  });
87
89
  if (screenRoot)
@@ -282,6 +284,11 @@ function key(finding) {
282
284
  function rule(code, target, message) {
283
285
  return { code: MOBILE_RULES[code].id, severity: MOBILE_RULES[code].severity, target, message, guide: "platform_guide('mobile-layout')" };
284
286
  }
287
+ /** Sabit görünümlü sahibinin bilerek boş bırakılabilen görünümü: veri yokken açılan görünüm (`MOBILE_OPTIONAL_VIEW_INDEX`). */
288
+ function isOptionalView(node, parent) {
289
+ const index = MOBILE_OPTIONAL_VIEW_INDEX[String(parent?.type ?? '')];
290
+ return index !== undefined && asNodes(parent?.children).filter(child => child.type === 'view')[index] === node;
291
+ }
285
292
  function asNodes(value) {
286
293
  return Array.isArray(value) ? value.filter((item) => !!item && typeof item === 'object') : [];
287
294
  }
@@ -114,6 +114,24 @@ export const MOBILE_RULES = {
114
114
  severity: 'info',
115
115
  sourceRef: 'zeyrek-mcp guides/mobile-layout.md §Sözleşme S5/S7/S11',
116
116
  summary: 'Ölçek dışı değer: özel boşluk ya da sayısal gap 0/4/8/16/24/32 dışında, ham yarıçap, px yazı boyu'
117
+ },
118
+ 'MOB-028': {
119
+ id: 'MOB-028',
120
+ severity: 'warn',
121
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/resolve-motion.ts (tanınmayan alan ve okunamayan değer atlanır)',
122
+ summary: "Mobil motionStyle'da tanınmayan alt anahtar ya da küme/aralık dışı değer — çözücü atlar, hareket eksik çıkar"
123
+ },
124
+ 'MOB-029': {
125
+ id: 'MOB-029',
126
+ severity: 'warn',
127
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/resolve-transition.ts (çözücü; ayna pageTransition.ts); zeyrek-backend FlowDtoValidator (StepTransition); simülatör ölçümü 2026-10-10',
128
+ summary: "redirect.transition ya da properties.mobile.transition kümede değil (default | none | fade | slide | slideUp) — adımda backend 422; modal sunumda yazılan geçiş kullanılmaz (bilgi)"
129
+ },
130
+ 'MOB-030': {
131
+ id: 'MOB-030',
132
+ severity: 'info',
133
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/hero-tag.ts (kural; ayna heroTag.ts); zeyrek-frontend/docs/adr/0022-hero-yolu.md; simülatör ölçümü 2026-10-10',
134
+ summary: "Aynı sayfada aynı statik heroTag'i birden çok düğüm taşıyor — sayfa yakınlaşmanın hedefi olduğunda ilk çizilen hizalanır; kaynak sayfada sorun değildir"
117
135
  }
118
136
  };
119
137
  /**
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Mobil sayfa geçişinin gateway aynası: sözlük ve çözücü. Kaynak frontend'dedir
3
+ * (`zeyrek-frontend/packages/ng-apps-ui-native/src/motion/` — `transition-fields.ts`, `resolve-transition.ts`;
4
+ * uygulama ayarının okuyucusu `src/shell/mobile-transition.ts`). Bu dosya onun kopyasıdır ve
5
+ * `test/page-transition-parity.test.ts` ile kilitlidir: frontend'in altın vakaları (`transition-golden.json`) aynı
6
+ * girdiden aynı çıktıyı ve harfi harfine aynı uyarıyı vermeli; aynı dosyanın `values` listesi backend enum'unun
7
+ * (`StepTransition`) kopyasına da kilitlidir. Test kırılınca test düzeltilmez, bu dosya senkronlanır.
8
+ *
9
+ * Değerler ürünün dilidir; yerel yığının adları (`slide_from_right`) burada geçmez. Kuralların ölçümü:
10
+ * simülatör, 2026-10-10 (`zeyrek-frontend/docs/features/mobile-runtime.md` §Sayfa geçişi).
11
+ */
12
+ export const PAGE_TRANSITIONS = ['default', 'none', 'fade', 'slide', 'slideUp'];
13
+ /** Boş değer ne değer ne uyarıdır (seçilmemiş); kümede olmayan değer `value` taşımaz. */
14
+ export function transitionValue(raw, source) {
15
+ if (raw === undefined || raw === null || (typeof raw === 'string' && raw.trim() === ''))
16
+ return {};
17
+ if (typeof raw === 'string' && PAGE_TRANSITIONS.includes(raw))
18
+ return { value: raw };
19
+ return {
20
+ warning: `${source}: ${typeof raw === 'string' ? `'${raw}'` : JSON.stringify(raw)} geçersiz — ${PAGE_TRANSITIONS.join(', ')}, atlandı`
21
+ };
22
+ }
23
+ export const TRANSITION_SOURCES = { step: 'redirect.transition', application: 'properties.mobile.transition' };
24
+ /**
25
+ * "Bu gezinmede hangi geçiş oynar." Öncelik: adım → uygulama varsayılanı → `default`; okunamayan değer uyarısıyla
26
+ * atlanır. Sekme hedefi ve `modal` sunum seçimi kullanmaz (sekme önce gelir). "Hareketi azalt"ta seçilmiş geçiş
27
+ * `none` olur, `default` sisteme kalır.
28
+ */
29
+ export function resolveTransition(input, context) {
30
+ const step = transitionValue(input.step, TRANSITION_SOURCES.step);
31
+ const application = transitionValue(input.application, TRANSITION_SOURCES.application);
32
+ const warnings = [step.warning, application.warning].filter((warning) => warning !== undefined);
33
+ if (input.tabTarget)
34
+ return { transition: 'default', fixedBy: 'tab', warnings };
35
+ if (input.presentation === 'modal')
36
+ return { transition: 'default', fixedBy: 'modal', warnings };
37
+ const chosen = step.value ?? application.value ?? 'default';
38
+ return { transition: context.reduceMotion && chosen !== 'default' ? 'none' : chosen, warnings };
39
+ }
40
+ /**
41
+ * `properties.mobile.transition`'ın HAM değeri; alan yoksa `undefined`. Frontend'in `readMobileTransition`'ı doğrulanmış
42
+ * değeri verir ve bozuk olanı sessizce düşürür; gateway uyarmak için ham değeri ister — adı bu yüzden farklı.
43
+ */
44
+ export function rawMobileTransition(properties) {
45
+ const mobile = properties?.mobile;
46
+ return mobile && typeof mobile === 'object' ? mobile.transition : undefined;
47
+ }
48
+ /**
49
+ * MOB-029'un uygulama ayarı ayağı. Backend `properties`'i doğrulamaz (jsonb serbest) ve cihazın okuyucusu sözlük
50
+ * dışı değeri SESSİZCE düşürür — uyarının verildiği tek yer burasıdır.
51
+ */
52
+ export function mobileTransitionProblems(properties) {
53
+ const { warning } = transitionValue(rawMobileTransition(properties), TRANSITION_SOURCES.application);
54
+ return warning
55
+ ? [`${warning} — cihaz değeri okumaz, geçişini seçmemiş yönlendirme adımları sistemin geçişiyle açılır`]
56
+ : [];
57
+ }
@@ -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', 'ad-show', 'back']),
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', 'back', 'scan-code']),
24
24
  aliases: STEP_ALIASES,
25
25
  infoOnly: new Set()
26
26
  },
@@ -173,6 +173,7 @@ const flowBranchNames = {
173
173
  'open-settings': ['always'],
174
174
  'ad-show': ['always', 'error'],
175
175
  back: ['always'],
176
+ 'scan-code': ['always', 'error'],
176
177
  end: []
177
178
  };
178
179
  /**
@@ -185,7 +186,7 @@ export const knownFlowStepTypes = new Set([
185
186
  'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
186
187
  'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function', 'share',
187
188
  'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'bpm',
188
- 'permission', 'open-settings', 'ad-show', 'back'
189
+ 'permission', 'open-settings', 'ad-show', 'back', 'scan-code'
189
190
  ]);
190
191
  /**
191
192
  * Cihaz yeteneği adımlarının kapalı kümeleri — `@ng-apps/flow` `tasks/device/device.functions.ts`
@@ -199,6 +200,12 @@ export const locationAccuracies = new Set(['low', 'balanced', 'high']);
199
200
  * ve `adBanner.unit` bu adla mağaza ayarındaki yerleşimi anar.
200
201
  */
201
202
  export const adPlacementNamePattern = /^[a-z][A-Za-z0-9]{0,39}$/;
203
+ /**
204
+ * `scan-code` adımının kod türleri — `@ng-apps/flow` `SCAN_CODE_TYPES`'ın aynası (kök ADR 0017); altın dosya
205
+ * `scan-code-golden.json` ile kilitlidir (`test/flow-step-parity.test.ts`), backend `ScanCodeType` aynı kümeyle 422 döner.
206
+ * `upc_a` bilerek yok: UPC-A etiketi iki platformda `ean13` ve on üç haneyle döner.
207
+ */
208
+ export const scanCodeTypes = new Set(['aztec', 'codabar', 'code39', 'code93', 'code128', 'datamatrix', 'ean8', 'ean13', 'itf14', 'pdf417', 'qr', 'upc_e']);
202
209
  /** `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. */
203
210
  export const permissionStepModes = new Set(['request', 'check']);
204
211
  /**
@@ -2710,6 +2717,50 @@ export const policyRules = [
2710
2717
  return findings;
2711
2718
  })
2712
2719
  },
2720
+ {
2721
+ id: 'FLOW-042',
2722
+ module: 'flow',
2723
+ appliesTo: ['create', 'update'],
2724
+ sourceRef: "Kod tarama adımının argümanı StepManagementArgument'ın mevcut alanlarındadır (kök docs/adr/0017-kod-tarama-akis-adimi.md): scan-code.value = izinli kod türleri (sözlükten virgüllü küme, DÜZ METİN; boş → hepsi), scan-code.title = ekrandaki yönerge (metin şablonu, FLOW-022). Backend FlowDtoValidator her adı ScanCodeType kümesiyle BİREBİR karşılaştırır (büyük harf ve ifade sarmalı 422, FlowStepScanCodeTypeUnknown); çalışma zamanı sözlük dışı adda ekranı açmaz, `invalid` döner. upc_a sözlükte yoktur: UPC-A etiketi iki platformda ean13 ve on üç haneyle (başında 0) döner. types / formats / barcodeTypes anahtarları sözleşmede yok, 200 ile sessizce düşer ve adım bütün türleri okur.",
2725
+ check: definition => flowAllSteps(definition)
2726
+ .filter(step => step.type === 'scan-code')
2727
+ .flatMap((step) => {
2728
+ const args = (step.args ?? {});
2729
+ const raw = typeof args.value === 'string' ? args.value : '';
2730
+ const names = raw.split(',').map(name => name.trim()).filter(name => name !== '');
2731
+ const unknown = names.filter(name => !scanCodeTypes.has(name));
2732
+ const stray = ['types', 'formats', 'barcodeTypes', 'codeTypes', 'type'].filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
2733
+ const allowed = [...scanCodeTypes].join(', ');
2734
+ const findings = [];
2735
+ if (unknown.length > 0) {
2736
+ const unquoted = raw.replace(/^['"`](.*)['"`]$/, '$1');
2737
+ const lowered = unknown.filter(name => scanCodeTypes.has(name.toLowerCase()));
2738
+ findings.push({
2739
+ ruleId: 'FLOW-042',
2740
+ severity: 'block',
2741
+ message: unquoted !== raw && unquoted.split(',').every(name => scanCodeTypes.has(name.trim()))
2742
+ ? `'${flowStepLabel(step)}' scan-code adımının kod türleri tırnaklı yazılmış: ${raw}. Bu alan DÜZ METİNDİR, ifade değil — args.value: '${unquoted}' yazın.`
2743
+ : /\{\{|\}\}|[()[\]]|\bstate\b|\bSteps\b/.test(raw)
2744
+ ? `'${flowStepLabel(step)}' scan-code adımının kod türleri ifade olarak yazılmış: ${raw}. Bu alan DÜZ METİNDİR ve olduğu gibi okunur (platform 422 döner) — türleri virgülle yazın ya da hepsi için boş bırakın. Kullanılabilir: ${allowed}.`
2745
+ : unknown.includes('upc_a')
2746
+ ? `'${flowStepLabel(step)}' scan-code adımında 'upc_a' sözlükte yok: UPC-A etiketi iki platformda 'ean13' türüyle ve on üç haneyle (başında 0) döner — 'ean13' yazın (platform 422 döner). Kullanılabilir: ${allowed}.`
2747
+ : lowered.length > 0
2748
+ ? `'${flowStepLabel(step)}' scan-code adımının kod türü büyük harfle yazılmış: ${lowered.join(', ')}. Adlar küçük harftir ve birebir karşılaştırılır (platform 422 döner) — ${lowered.map(name => `'${name.toLowerCase()}'`).join(', ')} yazın.`
2749
+ : `'${flowStepLabel(step)}' scan-code adımının kod türü tanınmıyor: ${unknown.join(', ')}; platform 422 döner. Kullanılabilir: ${allowed} (boş → hepsi).`,
2750
+ guide: 'ngapps://guides/mobile-steps'
2751
+ });
2752
+ }
2753
+ if (stray.length > 0) {
2754
+ findings.push({
2755
+ ruleId: 'FLOW-042',
2756
+ severity: names.length > 0 ? 'warn' : 'block',
2757
+ message: `'${flowStepLabel(step)}' scan-code adımında args.${stray.join('/args.')} alanı var: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu 200 ile sessizce düşürür ve adım bütün kod türlerini okur. Türler args.value'ya virgüllü düz metin olarak yazılır (ör. 'qr,ean13'). Kullanılabilir: ${allowed}.`,
2758
+ guide: 'ngapps://guides/mobile-steps'
2759
+ });
2760
+ }
2761
+ return findings;
2762
+ })
2763
+ },
2713
2764
  {
2714
2765
  id: 'FLOW-041',
2715
2766
  module: 'flow',
@@ -1,3 +1,4 @@
1
+ import { motionFieldBinding, resolveMotion } from './motionResolve.js';
1
2
  import { TRANSFORM_RANGES, rawProp, resolveNodeStyleLayers, transformValue } from './styleResolve.js';
2
3
  import { mergeTypeScale, resolveTheme } from './themeResolve.js';
3
4
  /* ---- css-value.ts ---- */
@@ -440,6 +441,8 @@ const composeSurface = (surface, shell, theme, context) => {
440
441
  context.warnings?.push(`surfaceStyle.${key}: mobilde karşılığı yok, atlandı`);
441
442
  }
442
443
  };
444
+ /** Tipografi grubunun `composeTypography`'nin okuduğu alanları (frontend derleyicisiyle parite: `test/style-resolve-native.test.ts`). */
445
+ export const TYPOGRAPHY_KEYS = new Set(['textColor', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textDecoration', 'textAlign', 'letterSpacing', 'lineHeight']);
443
446
  const composeTypography = (typography, text, theme, context) => {
444
447
  const color = asText(typography.textColor, theme, context.warnings, 'typographyStyle.textColor');
445
448
  if (color)
@@ -510,20 +513,101 @@ export function composeNativeStyle(input, theme) {
510
513
  * Bir düğümün mobil çizerdeki stili (`NativeRenderer` → `nativeNodeStyle`, `renderer.functions.ts`). Temanın katmanı
511
514
  * (`componentStyles[tip]` ← `$preset`) web'in çözücüsünden gelir ve TABANDIR; panel grupları üstüne, düğümün serbest
512
515
  * `style`'ı son söz; `$preset` işareti RN stiline inmez (plans/mobil-plantcare-acigi f-16 — 2026-09-28'e dek çizer bu
513
- * katmanı okumuyordu, rapor da okumuyordu). İfadeye bağlı prop çalışma anında çözülür, burada yok.
516
+ * katmanı okumuyordu, rapor da okumuyordu). İfadeye bağlı prop çalışma anında çözülür, burada yok: grubu bağlı düğümde
517
+ * grup (tasarım yedeği taşısa da) stile girmez; yüzey ve tipografi grubunun TEK alanı bağlıysa (`{ type: 'code', ref }`,
518
+ * frontend ADR 0020) o alan stile girmez ve `bound`'da `<grup>.<alan>` diye listelenir — yedeği çizerin verdiği değer
519
+ * değildir (yedeksiz sarmal `"[object Object]"` yazılıyordu, inceleme 2026-10-09). Çizerin TANIMADIĞI alan bağlı olsa da
520
+ * bağlı alan değildir (`bound`'a girmez; yüzeyde derleyicinin "karşılığı yok" uyarısı düşer). `ref`'i boş `code` sarmalı
521
+ * bağ değildir: verisi ham değer okunur; verisiz sarmal alanı yok sayar (çizer `mapSchemaPropToRawProp` ile aynı).
522
+ *
523
+ * `motion`, kabuğun `motionStyle`'dan uyguladığı harekettir (`NativeRenderer` → `nodeMotionOf`, `/dev` ve cihaz):
524
+ * yalnız hareketi olan düğümde yazılır. Kanvas ve "hareketi azalt" onu dondurur — rapor dondurulmamış çözümdür.
514
525
  */
515
526
  export function nodeNativeStyle(node, theme, source) {
516
527
  const props = node.props;
528
+ const bound = [];
517
529
  const group = (name) => {
518
530
  const value = rawProp(props, name);
519
531
  return isPlainObject(value) ? value : null;
520
532
  };
533
+ const evaluated = (name, known) => {
534
+ const value = isBound(props?.[name]) ? null : group(name);
535
+ if (!value)
536
+ return value;
537
+ const kept = { ...value };
538
+ for (const [field, wrapper] of fieldWrappers(props?.[name])) {
539
+ if (!isExpression(wrapper)) {
540
+ if (!('data' in wrapper))
541
+ delete kept[field];
542
+ }
543
+ else if (known.has(field)) {
544
+ delete kept[field];
545
+ bound.push(`${name}.${field}`);
546
+ }
547
+ else {
548
+ // Tanınmayan alan: sarmalı değer olarak kalır ki derleyicinin kendi "karşılığı yok" uyarısı düşsün.
549
+ kept[field] = wrapper;
550
+ }
551
+ }
552
+ return kept;
553
+ };
521
554
  const layers = resolveNodeStyleLayers(rawProp(props, 'style'), source, node.type);
522
- return composeNativeStyle({
555
+ const composed = composeNativeStyle({
523
556
  themeStyle: layers.theme,
524
- surfaceStyle: group('surfaceStyle'),
525
- typographyStyle: group('typographyStyle'),
557
+ surfaceStyle: evaluated('surfaceStyle', SURFACE_KEYS),
558
+ typographyStyle: evaluated('typographyStyle', TYPOGRAPHY_KEYS),
526
559
  hoverStyle: group('hoverStyle'),
527
560
  style: layers.own
528
561
  }, theme);
562
+ const style = bound.length > 0 ? { ...composed, bound } : composed;
563
+ const resolved = nodeMotion(props?.motionStyle);
564
+ if (!resolved)
565
+ return style;
566
+ const { warnings, ...motion } = resolved;
567
+ const reported = { ...style, warnings: [...style.warnings, ...warnings] };
568
+ // Hareket çıkmadıysa (yalnız atlanan alanlar) `motion` yazılmaz; uyarıları yine listededir.
569
+ return Object.keys(motion).length > 0 ? { ...reported, motion } : reported;
570
+ }
571
+ const RUNNING = { design: false, reduceMotion: false };
572
+ /**
573
+ * İfadeye bağlı GRUP zarfı (`code`, `translation`): değeri çalışma anında oluşur. Zarfın `data`'sı tasarım zamanı
574
+ * yedeğidir (kanvasın gösterdiği değer), çizerin uyguladığı değer değil — rapora girmez.
575
+ */
576
+ const isBound = (value) => isPlainObject(value) && (value.type === 'code' || value.type === 'translation');
577
+ /**
578
+ * Statik grubun sarmallı alanları (`{ type, data?, ref? }`), ham hâlleriyle. Tasarım şeması statik sarmala
579
+ * `type: 'static'` yazar, yayınlanan sayfanın yükü yazmaz (backend `AppProp`, 2026-10-04'ten beri; statik alanda
580
+ * `type: null` gelir) — ikisi de statiktir (frontend `inner-bindings.ts` `fieldsOf` ile aynı kural). Yalnız yazılı
581
+ * `type`'ı tanıyan ilk sürüm gerçek yükte bağlı alanı hiç görmüyordu (ölçüldü 2026-10-09).
582
+ */
583
+ const fieldWrappers = (prop) => Object.entries(isPlainObject(prop) && (prop.type ?? 'static') === 'static' && isPlainObject(prop.data) ? prop.data : {})
584
+ .filter((entry) => isPlainObject(entry[1]) && typeof entry[1].type === 'string');
585
+ /** İfadeye bağlı alan sarmalı: çizer onu çalışma anında değerlendirir (frontend ADR 0020, `inner-bindings.ts` `isCodeBinding`). */
586
+ const isExpression = (wrapper) => wrapper.type === 'code' && typeof wrapper.ref === 'string' && wrapper.ref !== '';
587
+ /**
588
+ * Düğümün ham `motionStyle` prop'undan (zarfıyla) çalışma anı çözümü. Bağlı grup atlanır. Grubun içindeki tek alan
589
+ * ifadeye bağlıysa (`code`) değeri çalışma anında oluşur: çözüme girmez, `bound`'da adıyla listelenir — tasarım yedeği
590
+ * çizerin uyguladığı değer değildir. Çözülmeyen yazım (şablon metni, `code` dışı bağ) uyarısıyla listenin başındadır;
591
+ * çizerin ham değer olarak okuduğu verisi varsa çözüme o girer (`motionFieldBinding`).
592
+ */
593
+ function nodeMotion(prop) {
594
+ if (!isPlainObject(prop) || isBound(prop) || !isPlainObject(prop.data))
595
+ return null;
596
+ const unresolved = [];
597
+ const bound = [];
598
+ const written = Object.fromEntries(Object.entries(prop.data).flatMap(([field, value]) => {
599
+ const binding = motionFieldBinding(field, value);
600
+ if (!binding)
601
+ return [[field, value]];
602
+ if (binding.kind === 'expression') {
603
+ bound.push(field);
604
+ return [];
605
+ }
606
+ unresolved.push(binding.warning);
607
+ return binding.fallback === undefined ? [] : [[field, { type: 'static', data: binding.fallback }]];
608
+ }));
609
+ const resolved = resolveMotion(rawProp({ motionStyle: { type: 'static', data: written } }, 'motionStyle'), RUNNING);
610
+ if (!resolved && unresolved.length === 0 && bound.length === 0)
611
+ return null;
612
+ return { ...resolved, ...(bound.length > 0 ? { bound } : {}), warnings: [...unresolved, ...(resolved?.warnings ?? [])] };
529
613
  }
@@ -20,6 +20,9 @@ export function actionScript(action, captureTarget = studioWebTarget) {
20
20
  // Seçeneklerin arandığı kök: web'de kutunun kabı (`scope`), mobilde belge (liste `Modal` katmanında).
21
21
  const optionRoot = captureTarget.optionsOutsideNode ? 'document' : 'scope';
22
22
  if (action.action === 'click') {
23
+ // `<elementId>__press` basış yüzeyinin KENDİSİDİR (frontend K2 sözleşmesi): içinde düğme taşıyan yüzeyde
24
+ // (`flip-card` — web'de iç içe `<button>` olmasın diye `div` çizilir) ilk düğmeye inilmez.
25
+ const pressSurface = String(action.target ?? '').endsWith(`${INTERNAL_ROLE_SEPARATOR}press`);
23
26
  return `(function () {
24
27
  var count = performance.getEntriesByType('resource').length;
25
28
  var node = document.querySelector(${selector});
@@ -29,7 +32,7 @@ export function actionScript(action, captureTarget = studioWebTarget) {
29
32
  // çipin "kaldır" düğmesidir — eskiden ona basılıp seçim siliniyordu (37 S-18). Tek basışlı
30
33
  // denetimin yüzeyi <id>__press'tir (ui-native press-role-guard), anahtarınki role="switch".
31
34
  var press = document.querySelector(${JSON.stringify(`[data-element-id="${String(action.target ?? '')}${INTERNAL_ROLE_SEPARATOR}press"]`)});
32
- var target = node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
35
+ var target = ${pressSurface} || node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
33
36
  : (press || node.querySelector('button, a, [role="button"], [role="switch"]') || node);
34
37
  // S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
35
38
  // ajan ikinci sekmeyi hiç seçemiyordu (2026-09-16 raporu madde 6). Sekme seçmek 'tab'in işi.