@appaflytech/zeyrek-ai-mcp 0.0.40 → 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 (60) hide show
  1. package/dist/knowledge/componentCatalog.js +95 -104
  2. package/dist/knowledge/componentEvents.js +34 -18
  3. package/dist/knowledge/mobileComponentCatalog.js +119 -20
  4. package/dist/knowledge/mobileComponentEvents.js +41 -0
  5. package/dist/knowledge/motionResolve.js +252 -0
  6. package/dist/knowledge/normalize.js +117 -71
  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 +217 -24
  17. package/dist/knowledge/styleResolve.js +0 -1
  18. package/dist/knowledge/styleResolveNative.js +88 -4
  19. package/dist/platform/capture/actions.js +27 -1
  20. package/dist/platform/capture/capture.js +10 -13
  21. package/dist/platform/capture/persistent-browser.js +130 -0
  22. package/dist/platform/capture/summary.js +4 -1
  23. package/dist/resources/examples/flow-list-to-form.json +8 -10
  24. package/dist/resources/examples/flow-load-table.json +1 -1
  25. package/dist/resources/examples/mobile-app-shell-drawer.json +1 -0
  26. package/dist/resources/examples/mobile-list-detail-form.json +7 -3
  27. package/dist/resources/examples/mobile-scan-code.json +285 -0
  28. package/dist/resources/guides/component.md +3 -3
  29. package/dist/resources/guides/entity.md +16 -0
  30. package/dist/resources/guides/expressions.md +8 -7
  31. package/dist/resources/guides/merge.md +98 -18
  32. package/dist/resources/guides/microservice.md +3 -3
  33. package/dist/resources/guides/middleware.md +2 -0
  34. package/dist/resources/guides/mobile-layout.md +2 -1
  35. package/dist/resources/guides/mobile-publish.md +1 -0
  36. package/dist/resources/guides/mobile-steps.md +67 -0
  37. package/dist/resources/guides/mobile.md +51 -70
  38. package/dist/resources/guides/page.md +229 -137
  39. package/dist/resources/guides/translation.md +67 -1
  40. package/dist/resources/guides/workflow.md +31 -1
  41. package/dist/resources/schema-fingerprint.json +2 -2
  42. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +30 -1
  43. package/dist/tools/activities.js +3 -1
  44. package/dist/tools/applications.js +16 -2
  45. package/dist/tools/components.js +12 -6
  46. package/dist/tools/designSchemaPatch.js +18 -13
  47. package/dist/tools/entities.js +41 -4
  48. package/dist/tools/layouts.js +29 -9
  49. package/dist/tools/merges.js +29 -9
  50. package/dist/tools/mobile.js +15 -1
  51. package/dist/tools/pages.js +156 -168
  52. package/dist/tools/platformInfo.js +3 -2
  53. package/dist/tools/profileWrite.js +2 -2
  54. package/dist/tools/queries.js +1 -1
  55. package/dist/tools/rows.js +14 -3
  56. package/dist/tools/schemaOps.js +132 -34
  57. package/dist/tools/screenWrite.js +3 -3
  58. package/dist/tools/screens.js +42 -3
  59. package/dist/tools/translations.js +148 -6
  60. package/package.json +2 -2
@@ -5,6 +5,7 @@ import { headersAccessNames, isHeadersAccessGuarded } from './flowGuards.js';
5
5
  import { callsItself, functionCallRefs, isValidParameterName, mentionsAwait, nameProblem, sourceOf, callPrefix } from './functionGuards.js';
6
6
  import { backendPolicyRules } from './backendConstraints.js';
7
7
  import { accessPolicyRules } from './access.js';
8
+ import { isGuid } from '../platform/refs.js';
8
9
  import { themePolicyRules } from './themeRules.js';
9
10
  import { processPolicyRules } from './process.js';
10
11
  import { mobileStorePolicyRules } from './mobileStore.js';
@@ -153,11 +154,13 @@ export function readsBeforeGuard(source, root) {
153
154
  * yaşatılır (microservice.ts:174 `isHasSuccess`). 'error' dalı 2026-09-26'dan beri motorun
154
155
  * KENDİSİ tanır (ADR 0004, ZM-07/g-02): bağlıysa hata anında `always` hiç koşmaz, sürer
155
156
  * (microservice.ts:182 `hasBranch`) — eski motor tanımadığı dalı hiç aramadığı için bu ekleme
156
- * geriye dönük zararsızdır.
157
+ * geriye dönük zararsızdır. `validation` tüm kurallar geçince 'success', biri düşünce 'error' dalına,
158
+ * ardından her durumda 'always'e gider (validation.ts; kök ADR 0014, g-18).
157
159
  */
158
160
  const flowBranchNames = {
159
161
  start: ['always'],
160
162
  microservice: ['always', 'success', 'error'],
163
+ validation: ['always', 'success', 'error'],
161
164
  confirm: ['yes', 'no', 'always'],
162
165
  function: ['always'],
163
166
  haptic: ['always'],
@@ -169,6 +172,8 @@ const flowBranchNames = {
169
172
  permission: ['always'],
170
173
  'open-settings': ['always'],
171
174
  'ad-show': ['always', 'error'],
175
+ back: ['always'],
176
+ 'scan-code': ['always', 'error'],
172
177
  end: []
173
178
  };
174
179
  /**
@@ -181,7 +186,7 @@ export const knownFlowStepTypes = new Set([
181
186
  'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
182
187
  'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function', 'share',
183
188
  'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'bpm',
184
- 'permission', 'open-settings', 'ad-show'
189
+ 'permission', 'open-settings', 'ad-show', 'back', 'scan-code'
185
190
  ]);
186
191
  /**
187
192
  * Cihaz yeteneği adımlarının kapalı kümeleri — `@ng-apps/flow` `tasks/device/device.functions.ts`
@@ -195,8 +200,20 @@ export const locationAccuracies = new Set(['low', 'balanced', 'high']);
195
200
  * ve `adBanner.unit` bu adla mağaza ayarındaki yerleşimi anar.
196
201
  */
197
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']);
198
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. */
199
210
  export const permissionStepModes = new Set(['request', 'check']);
211
+ /**
212
+ * `validation` adımının kural tipleri — `@ng-apps/flow` `VALIDATION_RULE_TYPES`'ın aynası (`test/flow-step-parity.test.ts`
213
+ * bağlar). Backend kümeyi doğrulamaz (kök ADR 0014); tanınmayan tip runtime'da `validate()` default dalında GEÇER sayılır.
214
+ */
215
+ export const validationRuleTypes = new Set(['required', 'email', 'regex', 'min-length', 'max-length', 'min-value', 'max-value']);
216
+ const validationParameterlessTypes = new Set(['required', 'email']);
200
217
  const flowFreeBranchTypes = new Set(['condition']);
201
218
  /** `condition.types.ts` ConditionArgumentRuleType — motorun tanıdığı üçlü. */
202
219
  const conditionRuleTypes = new Set(['if', 'else-if', 'else']);
@@ -204,16 +221,22 @@ const conditionRuleTypes = new Set(['if', 'else-if', 'else']);
204
221
  // backend'de bir Guid'dir (StepManagementSchema.cs `public Guid Id { get; set; }`), yani
205
222
  // okunabilir bir ad iki uçta birden tutmaz: istek System.Guid çevrimiyle reddedilir.
206
223
  const conditionRuleIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
207
- // alert.ts ve confirm.ts `task.argument.message`'ı doğrudan toast/dialog'a geçirir; argüman
208
- // Runtime.svelte'de HAM verilir (`argument: step.args`), yani bu alanlarda ifade değerlendirilmez.
209
- const flowLiteralTextFields = new Map([
224
+ // Metin şablonu alanları (kök ADR 0003 §Metin şablonu, K4 2026-10-08): iki barındırıcı (web alert.ts/confirm.ts,
225
+ // cihaz runtime-tasks.ts + device.functions.ts) metni @ng-apps/flow `resolveStepText` → `runPropertyCode`'dan
226
+ // geçirir — yalnız `{{ }}` içi çözülür, dışındaki çıplak ifade ekranda harfi harfine görünür.
227
+ const flowTemplateTextFields = new Map([
210
228
  ['alert', ['message', 'description']],
211
229
  ['confirm', ['message', 'title', 'confirmText', 'cancelText']],
212
- ['share', ['title']],
213
- ['notification-schedule', ['name', 'title', 'path']],
230
+ ['notification-schedule', ['name', 'title']],
214
231
  ['notification-cancel', ['name']]
215
232
  ]);
233
+ // Düz metin alanları: ne ifade ne şablon çözülür (`share.title` resolveShareContent'te, bildirim `path`'i K4 kapsamı dışında).
234
+ const flowLiteralTextFields = new Map([
235
+ ['share', ['title']],
236
+ ['notification-schedule', ['path']]
237
+ ]);
216
238
  const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
239
+ const flowTemplateSegments = /\{\{[\s\S]*?\}\}/g;
217
240
  // Barındırıcıya özel tipler BURADA kural üretmez. `navigate`i gateway normalizasyonu zaten
218
241
  // `redirect`e çeviriyor (normalize.ts `normalizeFlowHostGaps`) — `redirect` iki barındırıcıda da
219
242
  // koşar, yani vaka uyarılmaz, ORTADAN KALKAR. Gateway uygulamanın tipini platform profilinden bilir
@@ -1843,7 +1866,7 @@ export const policyRules = [
1843
1866
  id: 'ENT-006',
1844
1867
  module: 'entity',
1845
1868
  appliesTo: ['create', 'update'],
1846
- sourceRef: 'sms oturumu: kullanıcının intprimarykey/guidprimarykey referans tabloları — PK identity + properties.generationType:0 (Always) ile otomatik üretilir; dbType↔commonType eşleşmesi zorunlu',
1869
+ sourceRef: 'sms oturumu: kullanıcının intprimarykey/guidprimarykey referans tabloları — PK identity + properties.generationType:0 (Always) ile otomatik üretilir; dbType↔commonType eşleşmesi zorunlu. 38 S-1: PostgreSqlEntityConnector.ValidateEntityAsync identity alanında GenerationType 0/1 değilse InvalidGenerationTypeException (400) atar — create ve update',
1847
1870
  check: definition => {
1848
1871
  const primaryKeys = asArray(definition.fields)
1849
1872
  .filter((field) => Boolean(field) && typeof field === 'object')
@@ -1853,7 +1876,15 @@ export const policyRules = [
1853
1876
  const properties = (key.properties ?? {});
1854
1877
  const dbType = String(key.dbType ?? '');
1855
1878
  const expected = dbType === 'UUID' ? 'guid' : ['INT2', 'INT4', 'INT8'].includes(dbType) ? 'number' : null;
1856
- if (key.identity !== true || properties.generationType === undefined) {
1879
+ if (key.identity === true && properties.generationType !== 0 && properties.generationType !== 1) {
1880
+ findings.push({
1881
+ ruleId: 'ENT-006',
1882
+ severity: 'block',
1883
+ message: `'${key.slug ?? key.name}' primary key'i identity ama properties.generationType ${properties.generationType === undefined ? 'yok' : `'${String(properties.generationType)}'`} — platform 400 "Please select a valid generation type for '${key.slug ?? key.name}'" döner. Alana properties: { generationType: 0 } (Always) ya da 1 (ByDefault) ekleyin.`,
1884
+ guide: 'ngapps://guides/entity'
1885
+ });
1886
+ }
1887
+ else if (key.identity !== true) {
1857
1888
  findings.push({
1858
1889
  ruleId: 'ENT-006',
1859
1890
  severity: 'warn',
@@ -2507,7 +2538,7 @@ export const policyRules = [
2507
2538
  : [{
2508
2539
  ruleId: 'FLOW-021',
2509
2540
  severity: 'warn',
2510
- message: "errorSteps yalnız 'start' adımı taşıyor — motor bunu hata yolu SAYMAZ (flow-engine.ts hasErrorFlow: start dışında en az bir adım arar). Hata yolu kurulmuş görünür ama uç 4xx dönünce onError false döner, akış 'always' dalından hata gövdesiyle devam eder ve kullanıcıya hiçbir mesaj ulaşmaz. start'ın ardına en az bir adım bağlayın: ucun kendi mesajını göstereceksen setter (value.type:'code' → Steps.error.message), sabit bir uyarı yetiyorsa alert — alert'in 'message' alanı DÜZ METİNDİR (alert.ts task.argument.message'ı doğrudan toast'a geçirir), oraya ifade yazarsanız kullanıcı ifadenin kendisini görür.",
2541
+ message: "errorSteps yalnız 'start' adımı taşıyor — motor bunu hata yolu SAYMAZ (flow-engine.ts hasErrorFlow: start dışında en az bir adım arar). Hata yolu kurulmuş görünür ama uç 4xx dönünce onError false döner, akış 'always' dalından hata gövdesiyle devam eder ve kullanıcıya hiçbir mesaj ulaşmaz. start'ın ardına en az bir adım bağlayın: alert yeter — 'message' alanı metin şablonudur (FLOW-022), ucun kendi mesajı '{{ Steps.error.message }}' ile gösterilir; {{ }} dışında yazılan ifadeyi kullanıcı harfi harfine görür. Mesajı bir bileşene yazacaksan setter (value.type:'code' → Steps.error.message).",
2511
2542
  guide: 'ngapps://guides/page'
2512
2543
  }];
2513
2544
  }
@@ -2516,26 +2547,32 @@ export const policyRules = [
2516
2547
  id: 'FLOW-022',
2517
2548
  module: 'flow',
2518
2549
  appliesTo: ['create', 'update'],
2519
- sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(message, { title, confirmText, cancelText })` (boş çerçeve metni dialog.functions.ts resolveDialogFrame'de sabit metne düşer). Argüman Runtime.svelte'de HAM geçer (`argument: step.args`) — bu alanlarda hiçbir ifade değerlendirilmez, yazılan metin ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu: rehber hata mesajını göstermek için alert adımını öneriyordu). share adımında (2026-09-25) yalnız `title` düz metindir: @ng-apps/flow resolveShareContent onu olduğu gibi verir, `message` ve `path` ifadedir",
2550
+ sourceRef: "Metin şablonu (kök docs/adr/0003-cihaz-yetenegi-adimlari.md §Metin şablonu, plans/mobil-acik-isler K4, 2026-10-08): alert.ts/confirm.ts (web) ve ui-native runtime-tasks.ts + @ng-apps/flow device.functions.ts (cihaz, önizleme) `alert.message/description`, `confirm.message/title/confirmText/cancelText`, `notification-schedule.name/title`, `notification-cancel.name`'i @ng-apps/flow `resolveStepText` → `runPropertyCode`'dan geçirir: yalnız `{{ }}` içi çözülür (çeviri `t('anahtar')`, `state`, `Steps`, `item`), dışındaki çıplak ifade ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu; 38 S-12, 37 S-11). Düz metin kalanlar: share `title` (resolveShareContent olduğu gibi verir; `message` ve `path` ifadedir) ve notification-schedule `path` (K4 kapsamı dışı) — orada `{{ }}` de çözülmez",
2520
2551
  check: definition => flowAllSteps(definition)
2521
2552
  .flatMap((step) => {
2522
2553
  const type = String(step.type ?? '');
2523
- const fields = flowLiteralTextFields.get(type);
2524
- if (!fields)
2525
- return [];
2526
2554
  const args = (step.args ?? {});
2527
- return fields
2528
- .filter(field => typeof args[field] === 'string' && flowExpressionLooking.test(args[field]))
2555
+ const label = flowStepLabel(step);
2556
+ const text = (field) => (typeof args[field] === 'string' ? args[field] : '');
2557
+ const templateFindings = (flowTemplateTextFields.get(type) ?? [])
2558
+ .filter(field => flowExpressionLooking.test(text(field).replace(flowTemplateSegments, '')))
2559
+ .map(field => ({
2560
+ ruleId: 'FLOW-022',
2561
+ severity: 'warn',
2562
+ message: `'${label}' ${type} adımının '${field}' alanı {{ }} dışında ifade içeriyor: bu alan METİN ŞABLONUDUR (kök ADR 0003 §Metin şablonu) — yalnız {{ }} içi çözülür, dışındaki ifade kullanıcıya harfi harfine görünür. İfadeyi sarın: '{{ Steps.error.message }}', 'ilac-{{ item.id }}', '{{ t('anahtar') }}' (çeviri).`,
2563
+ guide: 'ngapps://guides/page'
2564
+ }));
2565
+ const literalFindings = (flowLiteralTextFields.get(type) ?? [])
2566
+ .filter(field => flowExpressionLooking.test(text(field)) || text(field).includes('{{'))
2529
2567
  .map(field => ({
2530
2568
  ruleId: 'FLOW-022',
2531
2569
  severity: 'warn',
2532
- message: type.startsWith('notification-')
2533
- ? `'${flowStepLabel(step)}' ${type} adımının '${field}' alanı ifade içeriyor ama bu alan DÜZ METİNDİR (kök ADR 0003): ${field === 'name' ? 'bildirimin kimliği' : field === 'path' ? 'açılacak sayfanın slug\'ı' : 'bildirim başlığı'} olduğu gibi kullanılır, ifade değerlendirilmez. Değişen içeriği ifade olan 'message' (gövde) ya da 'query' (sayfanın sorgusu) alanına yazın.`
2534
- : type === 'share'
2535
- ? `'${flowStepLabel(step)}' share adımının '${field}' alanı ifade içeriyor ama bu alan DÜZ METİNDİR: başlık paylaşım penceresine olduğu gibi gider (iOS'ta e-posta konusu, Android'de seçici başlığı), ifade değerlendirilmez. Değişen içeriği ifade olan 'message' alanına yazın — ör. message: "'Şuna bak: ' + Page.properties.name"; bağlantı 'path' alanındadır.`
2536
- : `'${flowStepLabel(step)}' ${type} adımının '${field}' alanı ifade içeriyor ama bu alan DÜZ METİNDİR: ${type}.ts task.argument.${field}'i doğrudan ekrana geçirir ve args hiçbir yerde değerlendirilmez (Runtime.svelte \`argument: step.args\`). Kullanıcı ifadenin sonucunu değil ifadenin KENDİSİNİ görür. Ucun mesajını göstermek için bir setter (ya da code) adımı kullanın — value { type: 'code', ref: 'Steps.error?.message' }; hata metninin nerede durduğu page.md §hata yolu'nda.`,
2570
+ message: type === 'share'
2571
+ ? `'${label}' share adımının '${field}' alanı ifade ya da {{ }} içeriyor ama bu alan DÜZ METİNDİR: başlık paylaşım penceresine olduğu gibi gider (iOS'ta e-posta konusu, Android'de seçici başlığı), ne ifade ne şablon çözülür. Değişen içeriği ifade olan 'message' alanına yazın — ör. message: "'Şuna bak: ' + Page.properties.name"; bağlantı 'path' alanındadır.`
2572
+ : `'${label}' ${type} adımının '${field}' alanı ifade ya da {{ }} içeriyor ama bu alan DÜZ METİNDİR (kök ADR 0003): açılacak sayfanın slug'ı olduğu gibi kullanılır, ne ifade ne şablon çözülür. Değişen kısmı ifade olan 'query' (sayfanın sorgusu) alanına yazın.`,
2537
2573
  guide: 'ngapps://guides/page'
2538
2574
  }));
2575
+ return [...templateFindings, ...literalFindings];
2539
2576
  })
2540
2577
  },
2541
2578
  {
@@ -2680,6 +2717,119 @@ export const policyRules = [
2680
2717
  return findings;
2681
2718
  })
2682
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
+ },
2764
+ {
2765
+ id: 'FLOW-041',
2766
+ module: 'flow',
2767
+ appliesTo: ['create', 'update'],
2768
+ sourceRef: "validation adımının kuralları StepManagementArgument.validations listesindedir (kök docs/adr/0014-validation-kurallari-cocuk-tabloda.md §5, 37 S-13): { id?, component (tasarım bileşeninin elementId'si; boş = null, sayfada olmayan 404), property, type, errorMessage (düz metin ya da t: anahtarı), settings (tek parametre, runtime Object.values(settings)[0]'ı okur) }. Backend type kümesini doğrulamaz; @ng-apps/flow validation.ts validate() tanımadığı tipte true döner, yani yazım hatalı kural sessizce GEÇER. Liste boşsa her çalıştırma success dalına gider. Komponent doğrulamasının { message, value } şekli bu sözleşmede YOKTUR ve 200 ile sessizce düşer",
2769
+ check: definition => flowAllSteps(definition)
2770
+ .filter(step => step.type === 'validation')
2771
+ .flatMap((step) => {
2772
+ const label = flowStepLabel(step);
2773
+ const validations = (step.args ?? {}).validations;
2774
+ if (validations !== undefined && validations !== null && !Array.isArray(validations)) {
2775
+ return [{
2776
+ ruleId: 'FLOW-041',
2777
+ severity: 'block',
2778
+ message: `'${label}' validation adımının args.validations alanı bir DİZİ olmalı: [{ component, property, type, errorMessage, settings }] (kök ADR 0014). Ör. [{ component: 'email', property: 'value', type: 'email', errorMessage: 't:validation.email', settings: {} }].`,
2779
+ guide: 'ngapps://guides/page'
2780
+ }];
2781
+ }
2782
+ const rules = (Array.isArray(validations) ? validations : []);
2783
+ if (rules.length === 0) {
2784
+ return [{
2785
+ ruleId: 'FLOW-041',
2786
+ severity: 'warn',
2787
+ message: `'${label}' validation adımında kural yok: args.validations boş olduğu için adım her çalıştırmada 'success' dalına gider, hiçbir alan denetlenmez. Kuralları args.validations'a yazın (kök ADR 0014).`,
2788
+ guide: 'ngapps://guides/page'
2789
+ }];
2790
+ }
2791
+ return rules.flatMap((rule, index) => {
2792
+ const type = typeof rule?.type === 'string' ? rule.type : '';
2793
+ const settings = rule?.settings && typeof rule.settings === 'object' ? Object.values(rule.settings) : [];
2794
+ const stray = ['message', 'value', 'name', 'isActive'].filter(key => rule?.[key] !== undefined);
2795
+ const at = `args.validations[${index}]`;
2796
+ const findings = [];
2797
+ if (!validationRuleTypes.has(type)) {
2798
+ findings.push({
2799
+ ruleId: 'FLOW-041',
2800
+ severity: 'block',
2801
+ message: `'${label}' validation adımında ${at}.type '${type}' tanınmıyor — beklenen: '${[...validationRuleTypes].join("' | '")}'. Platform tipi doğrulamadan kaydeder ve runtime tanımadığı tipi GEÇTİ sayar: kural hiçbir şeyi denetlemez.`,
2802
+ guide: 'ngapps://guides/page'
2803
+ });
2804
+ }
2805
+ else if (!validationParameterlessTypes.has(type) && (settings.length === 0 || settings[0] === null || settings[0] === '')) {
2806
+ findings.push({
2807
+ ruleId: 'FLOW-041',
2808
+ severity: 'block',
2809
+ message: `'${label}' validation adımında ${at} ('${type}') parametresiz: runtime eşiği settings'in İLK değerinden okur (${type === 'regex' ? "{ pattern: '^\\\\d+$' }" : '{ value: 8 }'}); değer yoksa karşılaştırma undefined ile yapılır ve kural her çalıştırmada düşer.`,
2810
+ guide: 'ngapps://guides/page'
2811
+ });
2812
+ }
2813
+ if (typeof rule?.property !== 'string' || rule.property.trim() === '') {
2814
+ findings.push({
2815
+ ruleId: 'FLOW-041',
2816
+ severity: 'block',
2817
+ message: `'${label}' validation adımında ${at}.property yok: runtime bileşenin hangi prop'unu okuyacağını bu alandan bilir (çoğunlukla 'value'). Yoksa okunan değer undefined olur.`,
2818
+ guide: 'ngapps://guides/page'
2819
+ });
2820
+ }
2821
+ if (stray.length > 0) {
2822
+ findings.push({
2823
+ ruleId: 'FLOW-041',
2824
+ severity: 'warn',
2825
+ message: `'${label}' validation adımında ${at}.${stray.join('/')} alanı var: bu komponent doğrulamasının şeklidir, adım kuralının sözleşmesinde YOK ve platform onu 200 ile sessizce düşürür. Mesaj errorMessage'a, eşik settings'e yazılır — ör. { component: 'sifre', property: 'value', type: 'min-length', errorMessage: 'En az 8 karakter', settings: { value: 8 } }.`,
2826
+ guide: 'ngapps://guides/page'
2827
+ });
2828
+ }
2829
+ return findings;
2830
+ });
2831
+ })
2832
+ },
2683
2833
  {
2684
2834
  id: 'FLOW-038',
2685
2835
  module: 'flow',
@@ -2741,7 +2891,7 @@ export const policyRules = [
2741
2891
  id: 'FLOW-030',
2742
2892
  module: 'flow',
2743
2893
  appliesTo: ['create', 'update'],
2744
- sourceRef: "Yerel bildirim adımlarının argümanı StepManagementArgument'ın mevcut alanlarındadır (kök docs/adr/0003-cihaz-yetenegi-adimlari.md): notification-schedule name = kimlik (düz metin, aynı kimlik eskisinin yerini alır), title düz metin, message ve value İFADE, path sayfa; notification-cancel name = kimlik ('*' hepsi). Zamanın türü value'nun sonucunun şeklinden okunur (@ng-apps/flow notificationTrigger): 'HH:mm' her gün, { weekday: 1-7, time: 'HH:mm' } her hafta (1 = Pazartesi), ISO tarih-saat bir kez. Eksik name/value backend FlowDtoValidator'da 400 alır; id/at/daily/weekly/body gibi anahtarlar sözleşmede YOKTUR ve platform onları 200 ile sessizce düşürür",
2894
+ sourceRef: "Yerel bildirim adımlarının argümanı StepManagementArgument'ın mevcut alanlarındadır (kök docs/adr/0003-cihaz-yetenegi-adimlari.md): notification-schedule name = kimlik ve title = başlık METİN ŞABLONU (yalnız {{ }} içi çözülür — 2026-10-08, FLOW-022; aynı kimlik eskisinin yerini alır), message ve value İFADE, path sayfa (düz metin); notification-cancel name = kimlik, metin şablonu ('*' hepsi). Zamanın türü value'nun sonucunun şeklinden okunur (@ng-apps/flow notificationTrigger): 'HH:mm' her gün, { weekday: 1-7, time: 'HH:mm' } her hafta (1 = Pazartesi), ISO tarih-saat bir kez. Eksik name/value backend FlowDtoValidator'da 400 alır; id/at/daily/weekly/body gibi anahtarlar sözleşmede YOKTUR ve platform onları 200 ile sessizce düşürür. GUID değerli args.id platformun kendi StepArgument kimliğidir (okunan akıştan geri yazım) ve işaretlenmez — 37 S-21",
2745
2895
  check: definition => flowAllSteps(definition)
2746
2896
  .filter(step => step.type === 'notification-schedule' || step.type === 'notification-cancel')
2747
2897
  .flatMap((step) => {
@@ -2749,13 +2899,16 @@ export const policyRules = [
2749
2899
  const text = (key) => typeof args[key] === 'string' ? String(args[key]).trim() : '';
2750
2900
  const findings = [];
2751
2901
  const schedule = step.type === 'notification-schedule';
2902
+ // 37 S-21: StepArgument bir AggregateRoot<Guid>'dir; okunan akış args.id'yi GUID olarak
2903
+ // taşır ve geri yazımda o platformun kimliğidir. Yalnız GUID olmayan id ajanın uydurmasıdır.
2752
2904
  const stray = ['id', 'identifier', 'at', 'daily', 'weekly', 'time', 'body', 'text']
2753
- .filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
2905
+ .filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '')
2906
+ .filter(key => key !== 'id' || !isGuid(String(args.id)));
2754
2907
  if (!text('name')) {
2755
2908
  findings.push({
2756
2909
  ruleId: 'FLOW-030',
2757
2910
  severity: 'block',
2758
- message: `'${flowStepLabel(step)}' ${step.type} adımının kimliği yok: args.name düz metin olarak yazılmalı (ör. 'sulama')${schedule ? ' — aynı kimlikle yeniden planlama eskisinin yerini alır' : " — '*' bütün planlı bildirimleri siler"}. Platform eksik kimliği 400 ile reddeder.`,
2911
+ message: `'${flowStepLabel(step)}' ${step.type} adımının kimliği yok: args.name metin olarak yazılmalı (ör. 'sulama'; kayıt başına 'ilac-{{ item.id }}')${schedule ? ' — aynı kimlikle yeniden planlama eskisinin yerini alır' : " — '*' bütün planlı bildirimleri siler"}. Platform eksik kimliği 400 ile reddeder.`,
2759
2912
  guide: 'ngapps://guides/mobile'
2760
2913
  });
2761
2914
  }
@@ -3262,6 +3415,46 @@ export const policyRules = [
3262
3415
  return findings;
3263
3416
  }
3264
3417
  },
3418
+ /**
3419
+ * WF-035 — 38 S-10: `query.fail` gateway'in `gatewayOnError` adımına bağlanınca bozuk id'li
3420
+ * istek 500 `JavaScriptException: error is not defined` döndü. Hedef adım global-error dalının
3421
+ * da hedefi olabilir; o yoldan gelince `error` kurulur, bağlı fail yolundan gelince kurulmaz —
3422
+ * bu yüzden hedefin global-error hedefi olması uyarıyı susturmaz.
3423
+ */
3424
+ {
3425
+ id: 'WF-035',
3426
+ module: 'workflow',
3427
+ appliesTo: ['create', 'update', 'preview'],
3428
+ sourceRef: "IStep.cs FailedOutput: global-error dalına yalnız fail dalı BAĞLANMAMIŞSA geçilir ve `error` değişkenini yalnız ActivityContext.EnterGlobalErrorHandling kurar. Bağlı fail dalında sonraki adımlar patlayan adımın hatasını kendi değişkeninden okur: ActivityContext.CreateJavascriptEngine her adımı adıyla { data, meta, error: { code, message, detail, type, cause } } olarak koyar. Ölçüm 38 S-10 (2026-09-30): query.fail → gatewayOnError, bozuk id → 500 'error is not defined'",
3429
+ check: (definition, context) => {
3430
+ const steps = workflowSteps(workflowBody(definition, context.workflowKind));
3431
+ const byId = new Map(steps.filter(step => step.id).map(step => [String(step.id), step]));
3432
+ const readsGlobalError = /(?<![\w$.])error\s*(?:\?\.|\.(?=[A-Za-z_$])|\[)/;
3433
+ const strings = (value) => {
3434
+ if (typeof value === 'string')
3435
+ return [value];
3436
+ if (Array.isArray(value))
3437
+ return value.flatMap(strings);
3438
+ if (value && typeof value === 'object')
3439
+ return Object.values(value).flatMap(strings);
3440
+ return [];
3441
+ };
3442
+ return steps.flatMap((step) => asArray(step.branches)
3443
+ .map(branch => branch)
3444
+ .filter(branch => branch?.type === 'fail' && Boolean(branch.nextStepId))
3445
+ .map(branch => byId.get(String(branch.nextStepId)))
3446
+ .filter((target) => Boolean(target) && strings(target.rule).some(text => readsGlobalError.test(text)))
3447
+ .map(target => {
3448
+ const name = stepLabel(step);
3449
+ return {
3450
+ ruleId: 'WF-035',
3451
+ severity: 'warn',
3452
+ message: `'${name}' adımının fail dalı '${stepLabel(target)}' adımına bağlı ve o adım 'error.*' okuyor. 'error' YALNIZ global-error dalında (fail dalı bağlanmamışken) kurulur; bağlı fail dalından gelindiğinde tanımsızdır ve adım 500 'JavaScriptException: error is not defined' ile düşer. Patlayan adımın hatasını kendi değişkeninden okuyun: ${name}.error.message · ${name}.error.code (ayrıca detail, type, cause; çıktı ${name}.data). Ya da fail dalını bağlamayın: hata global-error dalına düşer ve 'error' orada kurulur. Global-error hedefiyle aynı adımı paylaşmak yerine fail dalına ayrı bir response adımı bağlayın (ör. 404).`,
3453
+ guide: 'ngapps://guides/workflow'
3454
+ };
3455
+ }));
3456
+ }
3457
+ },
3265
3458
  {
3266
3459
  id: 'WF-021',
3267
3460
  module: 'workflow',
@@ -575,7 +575,6 @@ export const TYPED_STYLE_COMPONENTS = {
575
575
  * eski birleşik kaskadla (tema+preset+own) çözülmeye devam eder.
576
576
  */
577
577
  export const NODE_SURFACE_TYPES = new Set([
578
- 'table',
579
578
  'datatable',
580
579
  'collapsible-container',
581
580
  'popover',
@@ -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,11 +20,20 @@ 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});
26
29
  if (!node) return { found: false, applied: false, requests: count };
27
- var target = node.matches('button, a, [role="button"]') ? node : (node.querySelector('button, a, [role="button"]') || node);
30
+ // g-04 sırası: düğümün kendisi basılabilirse o → <id>__press → içteki ilk basılabilir öğe.
31
+ // Mobil çoklu seçim tetikleyicisi kökte role="combobox" taşır ve içindeki ilk role="button"
32
+ // çipin "kaldır" düğmesidir — eskiden ona basılıp seçim siliniyordu (37 S-18). Tek basışlı
33
+ // denetimin yüzeyi <id>__press'tir (ui-native press-role-guard), anahtarınki role="switch".
34
+ var press = document.querySelector(${JSON.stringify(`[data-element-id="${String(action.target ?? '')}${INTERNAL_ROLE_SEPARATOR}press"]`)});
35
+ var target = ${pressSurface} || node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
36
+ : (press || node.querySelector('button, a, [role="button"], [role="switch"]') || node);
28
37
  // S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
29
38
  // ajan ikinci sekmeyi hiç seçemiyordu (2026-09-16 raporu madde 6). Sekme seçmek 'tab'in işi.
30
39
  if (target.closest && target.closest('[role="tablist"]') && !node.matches('[role="tab"]')) {
@@ -354,15 +363,32 @@ export function actionScript(action, captureTarget = studioWebTarget) {
354
363
  if (!field) return finish({ found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count });
355
364
  var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
356
365
  : (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
366
+ // K1 (g-04): gerçek kullanıcı alana odaklanır, yazar ve çıkar. Blur'suz yazımda odak kaybına
367
+ // bağlı akış (ui-native girişlerin 'changed'i) hiç koşmuyordu (37 S-19). focus() odağı alamazsa
368
+ // (görünmez alan, odaksız belge) olaylar elle atılır: Svelte 'blur'u, React 'focusout'u dinler.
369
+ var focusField = function () {
370
+ field.focus();
371
+ if (document.activeElement === field) return;
372
+ field.dispatchEvent(new FocusEvent('focus'));
373
+ field.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
374
+ };
375
+ var blurField = function () {
376
+ if (document.activeElement === field) { field.blur(); return; }
377
+ field.dispatchEvent(new FocusEvent('blur'));
378
+ field.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
379
+ };
380
+ focusField();
357
381
  if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
358
382
  var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
359
383
  if (field.checked !== shouldCheck) field.click();
384
+ blurField();
360
385
  return finish({ found: true, applied: true, requests: count });
361
386
  }
362
387
  var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
363
388
  setter.call(field, ${value});
364
389
  field.dispatchEvent(new Event('input', { bubbles: true }));
365
390
  field.dispatchEvent(new Event('change', { bubbles: true }));
391
+ blurField();
366
392
  return finish({ found: true, applied: true, requests: count });
367
393
  })()`;
368
394
  }
@@ -2,6 +2,7 @@ import { PlatformApiError } from '../errors.js';
2
2
  import { parseDomSummary } from '../domSummary.js';
3
3
  import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../../knowledge/componentCatalog.js';
4
4
  import { logStep } from '../../observability/trace.js';
5
+ import { PersistentScreenshotBrowser, persistentContextOpener, screenshotProfileRoot } from './persistent-browser.js';
5
6
  import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
6
7
  import { domSummaryScript } from './summary.js';
7
8
  import { createPageConsoleLog, describePageFailure, isExpressionEcho, parseRenderErrors, renderErrorsScript, splitExpressionErrors } from './console-log.js';
@@ -280,22 +281,18 @@ export class StudioScreenshotter {
280
281
  catch {
281
282
  throw new PlatformApiError('playwright-core paketi yüklü değil — zeyrek-mcp dizininde `npm install playwright-core` çalıştırın, ardından tarayıcı için `npx playwright-core install chromium`.');
282
283
  }
283
- logStep('tarayıcı →', 'chromium başlatılıyor (ilk çağrı — sonraki çağrılar önbellekteki tarayıcıyı kullanır)');
284
+ logStep('tarayıcı →', `chromium başlatılıyor (ilk çağrı — sonraki çağrılar önbellekteki tarayıcıyı kullanır) · profil ${screenshotProfileRoot()}`);
285
+ const open = persistentContextOpener(chromium, this.config.screenshotBrowserPath ? { executablePath: this.config.screenshotBrowserPath } : {});
286
+ const browser = new PersistentScreenshotBrowser(open);
284
287
  try {
285
- return await chromium.launch({
286
- headless: true,
287
- ...(this.config.screenshotBrowserPath ? { executablePath: this.config.screenshotBrowserPath } : {})
288
- });
288
+ // İlk bağlam burada açılır ki başlatma hatası eskisi gibi `launch()`'tan dönsün.
289
+ await (await browser.newContext({ viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1 })).close();
289
290
  }
290
291
  catch (firstError) {
291
- try {
292
- return await chromium.launch({ headless: true, channel: 'chrome' });
293
- }
294
- catch {
295
- const message = firstError instanceof Error ? firstError.message : String(firstError);
296
- throw new PlatformApiError(`Chromium başlatılamadı: ${message}. \`npx playwright-core install chromium\` ile indirin; alternatif olarak sistemdeki tarayıcının yolunu SCREENSHOT_BROWSER_PATH ile verin (alpine imajında \`apk add chromium\` + /usr/bin/chromium-browser).`);
297
- }
292
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
293
+ throw new PlatformApiError(`Chromium başlatılamadı: ${message}. \`npx playwright-core install chromium\` ile indirin; alternatif olarak sistemdeki tarayıcının yolunu SCREENSHOT_BROWSER_PATH ile verin (alpine imajında \`apk add chromium\` + /usr/bin/chromium-browser).`);
298
294
  }
295
+ return browser;
299
296
  }
300
297
  /**
301
298
  * Bekleme stratejisi bilinçli olarak `networkidle` DEĞİL: Studio'nun kök layout'u
@@ -331,7 +328,7 @@ export class StudioScreenshotter {
331
328
  before.note = action.target === '__tabs' || action.target.startsWith(`__tabs${INTERNAL_ROLE_SEPARATOR}`)
332
329
  ? `'${action.target}' sayfada yok: bu sayfa sekme çubuğunda değil ya da uygulamada sekmeler kapalı (application_get → properties.mobile.tabs). Sekme öğesinin kimliği __tabs__item-<sayfa slug'ı>.`
333
330
  : action.target.includes(INTERNAL_ROLE_SEPARATOR)
334
- ? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Kolon rolleri kolonun SLUG'ından türer (başlığından değil); satır sırası 0 tabanlıdır ve sayfa değişince yeniden numaralanır.`
331
+ ? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Roller görünen metinden değil kayıttaki anahtardan türer (sekme key'i, kart id'si); hedef bileşenin rollerini component_get çıktısındaki internalRoles verir.`
335
332
  : `'${action.target}' elementId'si sayfada yok. page_schema_get ile elementId'leri listeleyin; bir komponentin İÇ kontrolünü hedefliyorsanız <elementId>__<rol> biçimini kullanın (component_get çıktısındaki internalRoles).`;
336
333
  }
337
334
  // Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"