@appaflytech/zeyrek-ai-mcp 0.0.41 → 0.0.43

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
@@ -3,8 +3,9 @@ import { PlatformApiError } from '../platform/errors.js';
3
3
  import { ensureWorkflowErrorPath } from './errorPath.js';
4
4
  import { DOCUMENT_STYLE_TYPES, TYPED_STYLE_COMPONENTS } from './styleResolve.js';
5
5
  import { STATE_STORAGE_KINDS, STEP_ALIASES } from './platform/profile.js';
6
- import { inlineStyleFields } from './componentCatalog.js';
6
+ import { MOBILE_OPTIONAL_VIEW_INDEX, MOBILE_ROW_LIST_TYPES } from './mobileComponentCatalog.js';
7
7
  import { FUNCTIONS_NAMESPACE } from './functionGuards.js';
8
+ import { retiredComponent } from './componentCatalog.js';
8
9
  const SORT_DEFAULT = { name: 'sortField', type: 'sortType', source: 'query-string' };
9
10
  const LIMIT_DEFAULT = { name: 'limit', offset: 'offset', source: 'query-string' };
10
11
  function asArray(value) {
@@ -634,16 +635,12 @@ export function codePropTemplateErrors(ops) {
634
635
  *
635
636
  * Seçici anahtarlara (`&__body`, `:hover`, `--token`) DOKUNULMAZ; yalnız `a-b` biçimli düz CSS
636
637
  * özellik adları camelCase'e çevrilir ve çevrim `gatewayNormalized` ile bildirilir.
637
- *
638
- * TB-129 — `inlineFields` verilirse o ÜST DÜZEY alanların altına hiç girilmez: `table.style.th`
639
- * gibi dallar satır içi `style` attribute'una basılır ve kebab-case zorunludur
640
- * (`INLINE_STYLE_FIELDS_BY_TYPE`). Çeviri orada stili öldürüyordu.
641
638
  */
642
639
  const styleKeyPattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
643
640
  export function isStylePropName(name) {
644
641
  return name === 'style' || /Style$/.test(name);
645
642
  }
646
- export function normalizeStyleKeys(value, label, fixed, inlineFields) {
643
+ export function normalizeStyleKeys(value, label, fixed) {
647
644
  if (Array.isArray(value)) {
648
645
  for (const entry of value)
649
646
  normalizeStyleKeys(entry, label, fixed);
@@ -652,10 +649,7 @@ export function normalizeStyleKeys(value, label, fixed, inlineFields) {
652
649
  if (!value || typeof value !== 'object')
653
650
  return;
654
651
  const record = value;
655
- const skipped = new Set(inlineFields ?? []);
656
652
  for (const key of Object.keys(record)) {
657
- if (skipped.has(key))
658
- continue;
659
653
  normalizeStyleKeys(record[key], label, fixed);
660
654
  if (!styleKeyPattern.test(key))
661
655
  continue;
@@ -798,10 +792,8 @@ export function normalizeComponentDefaults(nodes) {
798
792
  if (node.props && typeof node.props === 'object' && !Array.isArray(node.props)) {
799
793
  const props = node.props;
800
794
  const styleKeys = [];
801
- // Satır içi stil dalları komponent TİPİNE göre atlanır; tip bilinmiyorsa eski davranış.
802
- const inlineFields = (name) => name === 'style' ? inlineStyleFields(typeof node.type === 'string' ? node.type : undefined) : undefined;
803
795
  for (const name of Object.keys(props).filter(isStylePropName)) {
804
- normalizeStyleKeys(props[name], `${String(node.elementId ?? node.name ?? '?')}.${name}`, styleKeys, inlineFields(name));
796
+ normalizeStyleKeys(props[name], `${String(node.elementId ?? node.name ?? '?')}.${name}`, styleKeys);
805
797
  }
806
798
  if (styleKeys.length > 0)
807
799
  missing.push(`stil anahtarları camelCase'e çevrildi: ${styleKeys.join(' · ')}`);
@@ -878,6 +870,40 @@ export function assertAddComponentIds(ops) {
878
870
  if (errors.length > 0)
879
871
  throw new PlatformApiError(`PAGE-037 — ${errors.join(' · ')} (istek gönderilmedi). ${COMPONENT_BODY_RULE}`, 400);
880
872
  }
873
+ /**
874
+ * PAGE-051 — frontend kataloğundan kaldırılmış tipteki düğümleri (çocuklar dahil) toplar. Kayıt
875
+ * defteri eşitlemesi kaldırılan bileşeni silmediği için platform o tipi kabul eder; runtime düğümü
876
+ * yerine geçen bileşenle çizer ve eski olaylar hiç yayılmaz (`RETIRED_COMPONENTS`). Ölçüldü
877
+ * 2026-10-13, lokal `test`: `add_component { type: 'table' }` + `set_event doubleClick` kuru koşuda
878
+ * `valid: true`. Yalnız EKLENEN gövdeye bakar: o tiple kayıtlı düğüme yazan op'lar durdurulmaz.
879
+ */
880
+ export function retiredTypeNodes(nodes) {
881
+ const retired = [];
882
+ for (const node of asObjects(nodes)) {
883
+ const type = typeof node.type === 'string' ? node.type : undefined;
884
+ const record = retiredComponent(type);
885
+ if (record)
886
+ retired.push(`'${String(node.elementId ?? node.name ?? '?')}': '${type}' tipi katalogdan kaldırıldı — '${record.replacedBy}' kullanın`);
887
+ retired.push(...retiredTypeNodes(node.children));
888
+ }
889
+ return retired;
890
+ }
891
+ export const RETIRED_TYPE_RULE = "Kaldırılan tip kayıt defterinde durduğu için platform onu kabul eder, ama runtime düğümü yerine geçen bileşenle çizer ve eski olayları yaymaz: bağ kaydedilir, sessizce ölü kalır. Yerine geçenin sözlüğü component_get ile okunur; tablo için platform_guide('page') Kural 1e. O tiple KAYITLI düğüme yazan op'lar (replace_prop, move_component…) reddedilmez.";
892
+ export function assertNoRetiredTypes(ops) {
893
+ const retired = asObjects(ops).flatMap(op => op.op === 'add_component' ? retiredTypeNodes([op.component]) : []);
894
+ if (retired.length > 0)
895
+ throw new PlatformApiError(`PAGE-051 — katalogdan kaldırılan tipte add_component (istek gönderilmedi): ${retired.join(' · ')}. ${RETIRED_TYPE_RULE}`, 400);
896
+ }
897
+ /**
898
+ * PAGE-051'in OLUŞTURMA ayağı (`page_create`, `layout_create`): orada ağacın tamamı yenidir.
899
+ * Güncelleme yolları (`page_update`, `layout_update`) bunu ÇAĞIRMAZ — tam şema yazımında yeni
900
+ * düğüm kayıtlıdan ayrılamaz, eski sayfa kaydedilemez olurdu.
901
+ */
902
+ export function assertNoRetiredNodes(nodes) {
903
+ const retired = retiredTypeNodes(nodes);
904
+ if (retired.length > 0)
905
+ throw new PlatformApiError(`PAGE-051 — katalogdan kaldırılan tipte düğüm (istek gönderilmedi): ${retired.join(' · ')}. ${RETIRED_TYPE_RULE}`, 400);
906
+ }
881
907
  /**
882
908
  * add_component micro-op'unun taşıdığı komponent ağacı da aynı ToDomain yolundan geçer.
883
909
  */
@@ -895,7 +921,8 @@ export function assertAddComponentIds(ops) {
895
921
  * `replace_prop` `expression` alanını HİÇ OKUMAZ — yalnız set_binding okur; eskiden sarmalı
896
922
  * `expression`'a taşıyorduk ve prop 200 dönerken `{type:'static', data:null}` kaydediliyordu.
897
923
  * İfade `ref`'tedir (PG-117); ref yoksa string `data`'dan alınır, ikisi de yoksa çağrı durur.
898
- * Platform code prop'unda yalnız `Ref`'i saklar (`Prop` kurucusu), `data` kalıcı değildir; ama
924
+ * Platform code prop'unda ifadeyi `Ref`'te saklar; `data` tasarım yedeğidir — yalnız ilkel değerken ve
925
+ * ifadeyi yinelemiyorsa kalır (2026-10-09, `DesignComponentProperty.SetRef`; öncesinde hiç saklanmıyordu).
899
926
  * `CreateProp` veri tipini `Data`'dan seçtiği için nesne/dizi `data` 500 verir — ref ile değiştirilir.
900
927
  * Aynı sebeple `value`'suz `expression` code sarmalına çevrilir, `value`'nun yanındaki `expression`
901
928
  * silinir. `translation` sarmalı da aynı gerekçeyle olduğu gibi gider (G-04): applier
@@ -947,7 +974,7 @@ function normalizeReplacePropValue(op) {
947
974
  notes.push(`${label}: code sarmalında ref yoktu, data'daki ifade ref'e kopyalandı — runtime ifadeyi ref'ten okur`);
948
975
  }
949
976
  if (code.data !== null && typeof code.data === 'object') {
950
- notes.push(`${label}: code sarmalının nesne/dizi data'sı ref ile değiştirildi — platform code prop'unda data'yı saklamaz, nesne data'yı Dictionary<string, Prop> diye çözüp 500 verir`);
977
+ notes.push(`${label}: code sarmalının nesne/dizi data'sı ref ile değiştirildi — platform code prop'unda yalnız ilkel data'yı (tasarım yedeği) saklar, nesne data'yı Dictionary<string, Prop> diye çözüp 500 verir`);
951
978
  code.data = code.ref;
952
979
  }
953
980
  if (code.data === undefined)
@@ -974,12 +1001,7 @@ function normalizeReplacePropValue(op) {
974
1001
  // düğümü yeniden kurmak ona yazan setter iteratee'lerinin element'ini siler (G-04, FLOW-018).
975
1002
  throw new PlatformApiError(`${label}: '${propType}' tipli Prop replace_prop ile kurulmaz — bu op static, code ve translation sarmalını taşır. Komponenti silip yeniden EKLEMEYİN: ona yazan setter adımlarının hedefi silinir (FLOW-018). Akış bağı için set_event, diğer tipler için page_update(schema) ile tam şema yazın.`, 400);
976
1003
  }
977
- /**
978
- * `typesByElementId` — TB-129: `replace_prop` op'u yalnız elementId taşır, stil çevirisinin
979
- * komponent tipine bakabilmesi için haritayı çağıran (okuduğu şemadan) verir. Verilmezse çeviri
980
- * tip ayrımı yapmaz; bu, harita okunamadığında yazmayı engellememek için bilinçli düşüştür.
981
- */
982
- export function normalizeSchemaOps(ops, typesByElementId) {
1004
+ export function normalizeSchemaOps(ops) {
983
1005
  const components = [];
984
1006
  const props = [];
985
1007
  const validations = [];
@@ -987,10 +1009,7 @@ export function normalizeSchemaOps(ops, typesByElementId) {
987
1009
  for (const op of asObjects(ops)) {
988
1010
  if (op.op === 'replace_prop') {
989
1011
  if (isStylePropName(String(op.prop ?? ''))) {
990
- const inline = String(op.prop) === 'style'
991
- ? inlineStyleFields(typesByElementId?.get(String(op.target ?? '')))
992
- : undefined;
993
- normalizeStyleKeys(op.value, `'${String(op.target ?? '?')}.${String(op.prop)}'`, props, inline);
1012
+ normalizeStyleKeys(op.value, `'${String(op.target ?? '?')}.${String(op.prop)}'`, props);
994
1013
  }
995
1014
  props.push(...normalizeReplacePropValue(op));
996
1015
  continue;
@@ -1342,7 +1361,7 @@ export function spacingLayerWarnings(nodes, unusedCode, numberCode) {
1342
1361
  const hint = node.type === 'slot'
1343
1362
  ? "Slot'un sayfa içeriğinden uzaklığı dış boşluğudur: appearance.marginType:'custom' + marginValue (sayı) yaz — slot'un zemini/çerçevesi olmadığı için görsel sonuç iç boşlukla aynıdır."
1344
1363
  : "İç boşluk yalnız droppable kaplarda ve tipli stil modelindeki yapraklarda (box · text · container · statcard · icon · form) çizilir; kenardan uzaklık için marginType/marginValue, iç boşluk için sarmalayıcı container kullan.";
1345
- warnings.push(`${unusedCode}: '${label}' (${String(node.type ?? '?')}) düğümüne custom iç boşluk yazıldı ama bu düğümde iç boşluk HİÇBİR kutuya çizilmez — platform 200 döner, ekran değişmez. ${hint}`);
1364
+ warnings.push(`${unusedCode}: '${label}' (${String(node.type ?? '?')}) düğümü custom iç boşluk taşıyor ama bu düğümde iç boşluk HİÇBİR kutuya çizilmez — platform 200 döner, ekran değişmez. ${hint}`);
1346
1365
  }
1347
1366
  walk(asObjects(node.children));
1348
1367
  }
@@ -2036,51 +2055,60 @@ export function splitJunctionFields(definition, isJunction) {
2036
2055
  return { base: { ...definition, fields: fields.filter(field => !isJunction(field)) }, deferred };
2037
2056
  }
2038
2057
  /**
2039
- * `remove_component` şemadan bir dalı çıkarır ama etkisi dalla sınırlı DEĞİLDİR: sayfa
2040
- * kaydedilirken artık hiçbir komponent olayının, prop'unun ya da sayfa olayının referans vermediği
2041
- * akışlar platformca KALICI olarak silinir (`PageService.UpdateAsync` → `flowRepository
2042
- * .DeleteRangeAsync`). Micro-op hattının "yalnız hedefe dokunur" garantisinin tek istisnası budur;
2043
- * çoğu zaman istenen davranıştır, istenmediğinde ise uyarı tek erken sinyaldir.
2044
- */
2045
- /**
2046
- * Repeater içinde repeater arar. ÖLÇÜLDÜ (2026-08-19, anket-admin probe sayfası): iç repeater
2047
- * OKUMADA çalışır (dış satır başına doğru sayıda iç satır çizilir) ama YAZMA ölüdür —
2048
- * iç girdiyi değiştirince dış satırın alanı değişmez ve iç repeater'ın `_repeaterValues`'ü null
2049
- * kalır. Sebep yapısal: şemada iç repeater TEK düğümdür, dış satır başına ayrı düğüm yoktur;
2050
- * `_repeaterValues` anahtarı iç satır id'sine göredir ve dış satırlar arasında çakışır.
2051
- * Üstelik tek bir düzenlemeden sonra DOM bozuluyor (ölçümde girdi sayısı 3 → 19) ve ekranda
2052
- * sürekli "Expression error: Cannot read properties of null" toast'ı çıkıyor.
2058
+ * Satır listesinin alt ağacındaki satır listesini arar (`MOBILE_ROW_LIST_TYPES`; web'de yalnız `repeater` vardır).
2059
+ * ÖLÇÜLDÜ (2026-08-19, anket-admin probe sayfası, repeater içinde repeater): iç liste OKUMADA çalışır (dış satır
2060
+ * başına doğru sayıda iç satır çizilir) ama YAZMA ölüdür — iç girdiyi değiştirince dış satırın alanı değişmez ve iç
2061
+ * listenin `_repeaterValues`'ü null kalır. Sebep yapısal: şemada iç liste TEK düğümdür, dış satır başına ayrı düğüm
2062
+ * yoktur; `_repeaterValues` anahtarı iç satır id'sine göredir ve dış satırlar arasında çakışır. Üstelik tek bir
2063
+ * düzenlemeden sonra DOM bozuluyor (ölçümde girdi sayısı 3 → 19) ve ekranda sürekli "Expression error: Cannot read
2064
+ * properties of null" toast'ı çıkıyor. Öteki iki tip aynı satır kapsamını kullanır (frontend `ROW_LIST_TYPES`), bu
2065
+ * yüzden kural üçünün çaprazına bakar (2026-10-09, plans/mobil-animasyon-sistemi faz 04 F-3).
2066
+ * Satır kapsamı satır ŞABLONUDUR: MOBİL listenin boş görünümü (`MOBILE_OPTIONAL_VIEW_INDEX`) sayfa kapsamındadır
2067
+ * (frontend `row-list-type.ts` `rowTemplateNodes`) — içindeki liste satır başına çoğalmaz ve iç içe sayılmaz; o alt ağaç
2068
+ * listenin kendi dış kapsamını taşır. Web `repeater`ının görünümü yoktur (şemasında `views` yok, çizeri bütün
2069
+ * çocukları satır başına çizer): orada `view` sıradan bir çocuktur, istisna uygulanmaz.
2053
2070
  * Doğru desen: iç listeyi tek bir `textarea`'da satır başına bir öğe olarak topla.
2054
2071
  */
2055
- export function nestedRepeaterNodes(nodes) {
2072
+ export function nestedRepeaterNodes(nodes, platform = 'web') {
2056
2073
  const found = [];
2057
- const walk = (list, insideRepeater) => {
2074
+ const walk = (list, insideType) => {
2058
2075
  for (const node of list) {
2059
- const isRepeater = node.type === 'repeater';
2060
- if (isRepeater && insideRepeater)
2061
- found.push(String(node.elementId ?? node.name ?? '?'));
2062
- walk(asObjects(node.children), insideRepeater || isRepeater);
2076
+ const type = String(node.type ?? '');
2077
+ const isRowList = MOBILE_ROW_LIST_TYPES.includes(type);
2078
+ if (isRowList && insideType !== undefined)
2079
+ found.push({ id: String(node.elementId ?? node.name ?? '?'), type, insideType });
2080
+ const children = asObjects(node.children);
2081
+ const emptyView = isRowList && platform === 'mobile' ? children.filter(child => child.type === 'view')[MOBILE_OPTIONAL_VIEW_INDEX[type] ?? -1] : undefined;
2082
+ for (const child of children)
2083
+ walk([child], isRowList && child !== emptyView ? type : insideType);
2063
2084
  }
2064
2085
  };
2065
- walk(asObjects(nodes), false);
2086
+ walk(asObjects(nodes), undefined);
2066
2087
  return found;
2067
2088
  }
2068
2089
  /**
2069
- * Mobil profilde gruplu listenin (gün/kategori başlığı altında satırlar) hazır bileşeni vardır:
2070
- * `grouped-list` (ui-native, ADR 0007). Web'de karşılığı yok — web mesajı textarea tarifinde kalır.
2090
+ * Mesaj hangi tipin hangi tipin içinde durduğunu söyler. Ölçüm cümlesi ve gruplu liste önerisi yalnız ölçülen ve
2091
+ * önerinin karşıladığı şekilde kalır (`repeater` içinde `repeater`): mobil profilde gruplu listenin (gün/kategori
2092
+ * başlığı altında satırlar) hazır bileşeni vardır, `grouped-list` (ui-native, ADR 0007); web'de karşılığı yok —
2093
+ * web mesajı textarea tarifinde kalır.
2071
2094
  */
2072
- export function nestedRepeaterMessage(ids, platform = 'web') {
2073
- const measured = `PAGE-013 — repeater içinde repeater (istek gönderilmedi): ${ids.join(' · ')}. `
2074
- + 'Ölçüldü (2026-08-19): iç repeater OKUMADA çalışır ama girilen değer dış satıra GERİ YAZILMAZ '
2075
- + '(_repeaterValues null kalır) ve tek düzenlemeden sonra DOM bozulur. Sebep yapısal: şemada iç '
2076
- + 'repeater tek düğümdür, dış satır başına ayrı düğüm yoktur. ';
2095
+ export function nestedRepeaterMessage(found, platform = 'web') {
2096
+ const pairs = [...new Set(found.map(entry => `${entry.insideType} içinde ${entry.type}`))];
2097
+ const located = pairs.map(pair => `${pair}: ${found.filter(entry => `${entry.insideType} içinde ${entry.type}` === pair).map(entry => entry.id).join(' · ')}`);
2098
+ const repeaterInRepeater = found.some(entry => entry.type === 'repeater' && entry.insideType === 'repeater');
2077
2099
  const textarea = "Satır içi listeyi tek bir textarea ile topla (satır başına bir öğe) ve kaydederken split('\n') ile böl — platform_guide('page') Kural 1g.";
2078
- if (platform !== 'mobile')
2079
- return measured + textarea;
2080
- return measured
2081
- + 'Başlıklar altında gruplanmış liste (güne/kategoriye göre) için mobilde tek `grouped-list` kur: düz `rows` + '
2082
- + '`groupBy` alan yolu; çocuk 0 bölüm başlığı (`{{ Section.title }}`), çocuk 1 öğe şablonu (`Row`, `{{ item.<alan> }}`) — '
2083
- + "platform_guide('mobile') §Güne göre görev listesi. Satır içinde düzenlenen alt liste gerekiyorsa: " + textarea;
2100
+ return `PAGE-013 — ${located.join('; ')} (istek gönderilmedi). `
2101
+ + 'Satır kapsamı tek seviyedir: şemada iç liste tek düğümdür, dış satır başına ayrı düğüm yoktur. '
2102
+ + (repeaterInRepeater
2103
+ ? 'Ölçüldü (2026-08-19): iç repeater OKUMADA çalışır ama girilen değer dış satıra GERİ YAZILMAZ '
2104
+ + '(_repeaterValues null kalır) ve tek düzenlemeden sonra DOM bozulur. '
2105
+ : 'İç listeyi dış listenin şablonundan çıkarın (kardeşi yapın ya da ayrı sayfaya/modala taşıyın). ')
2106
+ + (repeaterInRepeater && platform === 'mobile'
2107
+ ? 'Başlıklar altında gruplanmış liste (güne/kategoriye göre) için mobilde tek `grouped-list` kur: düz `rows` + '
2108
+ + '`groupBy` alan yolu; çocuk 0 bölüm başlığı (`{{ Section.title }}`), çocuk 1 öğe şablonu (`Row`, `{{ item.<alan> }}`) — '
2109
+ + "platform_guide('mobile') §Güne göre görev listesi. Satır içinde düzenlenen alt liste gerekiyorsa: "
2110
+ : '')
2111
+ + textarea;
2084
2112
  }
2085
2113
  /**
2086
2114
  * `style` prop'u düz CSS STRING verilmiş düğümler. Çalışır (emotion string'i CSS metni sayar) ama
@@ -37,7 +37,7 @@ function listSkeleton() {
37
37
  return [
38
38
  { op: 'add_component', parent: 'page_container', component: node('list_toolbar', 'container', { layout: staticProp('flex'), direction: staticProp('row') }, true, { h: 4, w: 12, x: 0, y: 0 }) },
39
39
  { op: 'add_component', parent: 'list_toolbar', component: node('btn_new', 'button', { text: staticProp('Yeni') }, false, { h: 4, w: 3, x: 0, y: 0 }) },
40
- { op: 'add_component', parent: 'page_container', component: node('records_table', 'table', {}, false, { h: 40, w: 12, x: 0, y: 4 }) }
40
+ { op: 'add_component', parent: 'page_container', component: node('records_table', 'datatable', {}, false, { h: 40, w: 12, x: 0, y: 4 }) }
41
41
  ];
42
42
  }
43
43
  function pageArgs(screenName, base, options, caveats) {
@@ -120,7 +120,7 @@ export function discoverPage(snapshot, screenRef, options = {}) {
120
120
  const events = readExample(LIST_EVENTS_EXAMPLE);
121
121
  const ops = [...listSkeleton(), ...(events.schemaPatchOps ?? [])];
122
122
  const presetBindings = bindPresets(ops, presets, caveats);
123
- caveats.push("records_table props boş — kolonları ve veri kaynağını component_get('table') + flow-load-table örneğine göre verin; set_event op'larındaki '<… flow id>' yer tutucuları flow_create sonrası gerçek id ile değiştirilir");
123
+ caveats.push("records_table props boş — kolonları ve veri kaynağını component_get('datatable') + flow-load-table örneğine göre verin (tek mount akışı: süzme/sıralama/sayfa değişimi mount'u yeniden tetikler); set_event op'larındaki '<… flow id>' yer tutucuları flow_create sonrası gerçek id ile değiştirilir");
124
124
  return {
125
125
  ...head,
126
126
  archetype: LIST_ARCHETYPE,
@@ -0,0 +1,70 @@
1
+ import { PlatformApiError } from '../../platform/errors.js';
2
+ /**
3
+ * Toplu yamanın zarfı (`{ op: 'batch', gestureId?, label?, ops: [...] }`) ARAÇ GİRİŞİNDE bir kez açılır: içindeki
4
+ * op'lar yerlerinde ve sırayla, tek tek yazılmış sayılır; gateway'in bütün geçişleri ve platforma giden gövde düz
5
+ * listeyi görür (sırayı platforma kadar `schemaOps.ts` `serverOpsInOrder` taşır). Platform için zarf yalnız
6
+ * gruplamadır — `PageSchemaMicroOpApplier.Apply` (backend) listeyi döngüsünün başında düzleştirir, `gestureId`/`label`
7
+ * hiçbir yerde okunmaz — bu yüzden düz liste aynı yazımdır.
8
+ *
9
+ * Girişte açılmadan önce her geçiş zarfa kendi bakıyordu ve çoğu bakmıyordu (TOOL-058, ölçüldü 2026-10-10, gerçek uç):
10
+ * zarf içindeki `replace_prop` sarılmadan gidip backend'in ham 422'sini aldı; kök kapsayıcıyı silen op, adsız düğüm
11
+ * ve gateway'in öykündüğü `resize_component` korumalardan geçmeden platforma ulaşıyordu.
12
+ *
13
+ * İç içe zarf reddedilir: platform onu `nested-batch` ile reddeder (`PageSchemaMicroOpApplier.ApplyOne`); gateway
14
+ * açarken kabul etseydi sözleşmeyi sessizce genişletirdi. `ops`'u liste olmayan zarf, op nesnesi olmayan öğe ve
15
+ * açılınca boş kalan yama da reddedilir — platform bunları sessizce yutar (`s.Ops ?? []`, `Where(w => w is not null)`)
16
+ * ve değişiklik olmadan sürüm ilerletir; gateway sessiz başarıyı geçirmez.
17
+ */
18
+ export function openBatchEnvelopes(tool, ops) {
19
+ const envelopes = ops.filter(op => op.op === 'batch');
20
+ if (envelopes.length === 0)
21
+ return { ops: [...ops], envelopes: 0 };
22
+ const issues = ops.flatMap((op, index) => (op.op === 'batch' ? envelopeIssues(op, index) : []));
23
+ if (issues.length > 0)
24
+ throw new PlatformApiError(`${tool}: batch gövdesi geçersiz (istek gönderilmedi): ${issues.join(' · ')}`, 400);
25
+ const opened = ops.flatMap(op => (op.op === 'batch' ? op.ops : [op]));
26
+ if (opened.length === 0) {
27
+ throw new PlatformApiError(`${tool}: uygulanacak micro-op yok — batch zarfının 'ops' listesi boş (istek gönderilmedi).`, 400);
28
+ }
29
+ return { ops: opened, envelopes: envelopes.length };
30
+ }
31
+ /** `ops[<zarfın sırası>]` ile başlar: birden çok zarf varken hangisinin bozuk olduğu mesajdan okunur. */
32
+ function envelopeIssues(envelope, at) {
33
+ const where = `ops[${at}] (batch)`;
34
+ if (!Array.isArray(envelope.ops))
35
+ return [`${where} 'ops' taşımalı: { op: 'batch', ops: [ …op'lar… ] }`];
36
+ return envelope.ops.flatMap((entry, index) => {
37
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
38
+ return [`${where}.ops[${index}] bir op nesnesi değil`];
39
+ return entry.op === 'batch'
40
+ ? [`${where}.ops[${index}] yine bir batch: iç içe batch yok (platform 'nested-batch' ile reddeder) — içteki op'ları dıştaki listeye taşıyın`]
41
+ : [];
42
+ });
43
+ }
44
+ /** Zarf açıldıysa yanıta düşen not: ajan zarfın ne kazandırdığını (hiçbir şey) görür. */
45
+ export function batchEnvelopeNote(opened) {
46
+ return opened.envelopes === 0
47
+ ? []
48
+ : [`batch zarfı açıldı (${opened.envelopes} zarf → ${opened.ops.length} op): op'lar tek tek yazılmış gibi işlendi ve platforma düz listeyle gitti — platform zarfı yalnız gruplama sayar (gestureId/label okunmaz).`];
49
+ }
50
+ /** Ret mesajına eklenen ipucu: `#<indeks>` ajanın GÖNDERDİĞİ listeye değil, zarf açıldıktan sonrakine göredir. */
51
+ export function batchIndexHint(opened) {
52
+ return opened.envelopes === 0
53
+ ? ''
54
+ : ` İndeksler batch zarfı açıldıktan sonraki (${opened.envelopes} zarf → ${opened.ops.length} op) açılmış listeye göredir: zarfın içindeki op'lar, zarfın durduğu yerde sırayla sayılır.`;
55
+ }
56
+ /**
57
+ * Zarfı açan SAF görünüm (iç içe dahil, hata vermez): op listesini okuyan bilgi kuralları için. Araç yolunda liste
58
+ * `openBatchEnvelopes`'tan geçtiği için zarf buraya ulaşmaz; kurallar yine de op sözlüğünün tamamını tanır — doğrudan
59
+ * çağrıldıklarında (testler, başka araçlar) zarfa kör kalmazlar (TOOL-057, ölçüldü 2026-10-10: doğrudan
60
+ * `MOB-001, MOB-028`, zarf içinde bulgu yok). Çağıranlar: `lint.ts` `profileFindings` ve `profileLintReadsSavedNodes`,
61
+ * `heroTag.ts`. Emsal: `normalize.ts` `orphanedFlowPlan`.
62
+ */
63
+ export function flattenBatchOps(ops) {
64
+ return ops.flatMap(op => (op.op === 'batch' ? flattenBatchOps(batchBody(op.ops)) : [op]));
65
+ }
66
+ function batchBody(value) {
67
+ if (!Array.isArray(value))
68
+ return [];
69
+ return value.filter((entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry));
70
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Paylaşılan öğe (Hero) etiketinin gateway aynası: değerin okunması, sayfanın etiketleri ve iki sayfanın eşlenmesi.
3
+ * Kaynak frontend'dedir (`zeyrek-frontend/packages/ng-apps-ui-native/src/motion/hero-tag.ts`, ADR
4
+ * `zeyrek-frontend/docs/adr/0022-hero-yolu.md`). Bu dosya onun kopyasıdır ve `test/hero-tag-parity.test.ts` ile
5
+ * kilitlidir: frontend'in altın vakaları (`hero-golden.json`) aynı girdiden aynı çıktıyı ve harfi harfine aynı uyarıyı
6
+ * vermeli. Test kırılınca test düzeltilmez, bu dosya senkronlanır.
7
+ */
8
+ import { flattenBatchOps } from './batchOps.js';
9
+ function asRecord(value) {
10
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
11
+ }
12
+ /** Metin kırpılır, sayı metne çevrilir; boş metin ve başka her tür "etiket yok"tur. */
13
+ export function heroTagValue(raw) {
14
+ if (typeof raw === 'number')
15
+ return Number.isFinite(raw) ? String(raw) : null;
16
+ if (typeof raw !== 'string')
17
+ return null;
18
+ return raw.trim() || null;
19
+ }
20
+ /** Düğümün ham `heroTag` prop'u (sarmalıyla): ifadeye bağlıysa değersiz, statikse metniyle; etiket yoksa `null`. */
21
+ export function heroTagEntry(elementId, prop) {
22
+ const wrapped = asRecord(prop);
23
+ if (!wrapped)
24
+ return null;
25
+ if (wrapped.type === 'code' || wrapped.type === 'translation') {
26
+ return typeof wrapped.ref === 'string' && wrapped.ref.trim() ? { elementId, bound: true } : null;
27
+ }
28
+ const tag = heroTagValue(wrapped.data);
29
+ return tag === null ? null : { elementId, tag };
30
+ }
31
+ /** Sayfa şemasındaki etiketler, belge sırasıyla (önce düğüm, sonra çocukları). */
32
+ export function heroTags(schema) {
33
+ if (!Array.isArray(schema))
34
+ return [];
35
+ return schema.flatMap(entry => {
36
+ const node = asRecord(entry);
37
+ if (!node)
38
+ return [];
39
+ const own = typeof node.elementId === 'string' && node.elementId ? heroTagEntry(node.elementId, asRecord(node.props)?.heroTag) : null;
40
+ return [...(own ? [own] : []), ...heroTags(node.children)];
41
+ });
42
+ }
43
+ function byTag(entries) {
44
+ const tags = new Map();
45
+ for (const entry of entries) {
46
+ if (entry.tag !== undefined)
47
+ tags.set(entry.tag, [...(tags.get(entry.tag) ?? []), entry.elementId]);
48
+ }
49
+ return tags;
50
+ }
51
+ function countBound(entries) {
52
+ return entries.filter(entry => entry.bound).length;
53
+ }
54
+ /**
55
+ * Bir sayfada aynı statik etiketi birden çok düğüm taşıyorsa: sayfa bir yakınlaşmanın HEDEFİ olduğunda hizalama kutusu
56
+ * tektir ve ilk çizilen düğüm hizalanır. Kaynak sayfada yineleme sorun değildir (kaynak dokunulan düğümdür).
57
+ */
58
+ export function heroTagProblems(entries, page) {
59
+ return [...byTag(entries)]
60
+ .filter(([, nodes]) => nodes.length > 1)
61
+ .map(([tag, nodes]) => `${page ? `${page}: ` : ''}heroTag '${tag}' ${nodes.length} düğümde (${nodes.join(', ')}) — ilk çizilen hizalanır`);
62
+ }
63
+ /**
64
+ * İki sayfanın etiketlerini eşler. Çift, statik etiketi iki sayfada da olan iki düğümdür. Yineleme yalnız HEDEF
65
+ * sayfada uyarıdır. Eşi olmayan statik etiket uyarıdır — karşı sayfada ifadeye bağlı etiket yoksa (varsa eş çalışma
66
+ * anında oluşabilir). Bugün yalnız parite testi çağırır: sayfalar arası eşleme yazımda yapılmaz (hedef sayfa henüz
67
+ * yazılmamış olabilir); ayna, frontend kuralıyla birlikte hazır dursun diye taşınır.
68
+ */
69
+ export function heroPairs(source, target) {
70
+ const sourceTags = byTag(source);
71
+ const targetTags = byTag(target);
72
+ const dynamic = { source: countBound(source), target: countBound(target) };
73
+ const unpaired = (tags, others, othersBound, where) => othersBound > 0 ? [] : [...tags].filter(([tag]) => !others.has(tag)).map(([tag, nodes]) => `heroTag '${tag}' (${nodes[0]}): ${where} eşi yok`);
74
+ return {
75
+ pairs: [...sourceTags].flatMap(([tag, nodes]) => {
76
+ const match = targetTags.get(tag);
77
+ return match ? [{ tag, source: String(nodes[0]), target: String(match[0]) }] : [];
78
+ }),
79
+ dynamic,
80
+ warnings: [
81
+ ...heroTagProblems(target, 'hedef sayfa'),
82
+ ...unpaired(sourceTags, targetTags, dynamic.target, 'hedef sayfada'),
83
+ ...unpaired(targetTags, sourceTags, dynamic.source, 'kaynak sayfada')
84
+ ]
85
+ };
86
+ }
87
+ function asList(value) {
88
+ return Array.isArray(value) ? value : value === undefined || value === null ? [] : [value];
89
+ }
90
+ function walkNodes(value, visit) {
91
+ for (const entry of asList(value)) {
92
+ const node = asRecord(entry);
93
+ if (!node)
94
+ continue;
95
+ if (typeof node.elementId === 'string' && node.elementId)
96
+ visit(node, node.elementId);
97
+ walkNodes(node.children, visit);
98
+ }
99
+ }
100
+ const HERO_PROP_OPS = new Set(['replace_prop', 'set_binding', 'set_prop_type']);
101
+ /**
102
+ * Yama bir `heroTag` yazıyor mu: prop'u yazan op (`replace_prop`, `set_binding`, `set_prop_type`) ya da etiketli düğüm
103
+ * ekleyen `add_component` — toplu yamanın (`batch`) içinde de. Yinelenen etiket denetimi (MOB-030) sayfanın öteki
104
+ * düğümlerine bakar; yama yolu kayıtlı ağacı yalnız bu durumda okur.
105
+ */
106
+ export function writesHeroTag(ops) {
107
+ return flattenBatchOps(ops).some(op => (HERO_PROP_OPS.has(String(op.op)) && op.prop === 'heroTag') ||
108
+ (op.op === 'add_component' && heroTags(asList(op.component)).length > 0));
109
+ }
110
+ /** Op'un `heroTag`'e yazdığı prop, saklanacağı sarmalla. `set_prop_type` kayıtlı prop'un tipini yerinde değiştirir. */
111
+ function writtenHeroProp(op, saved) {
112
+ if (op.op === 'set_binding')
113
+ return { type: 'code', ref: typeof op.expression === 'string' ? op.expression : asRecord(op.value)?.ref ?? op.value };
114
+ if (op.op === 'set_prop_type') {
115
+ const data = op.data ?? op.value;
116
+ return { ...asRecord(saved), type: op.propType, ...(op.ref === undefined ? {} : { ref: op.ref }), ...(data === undefined ? {} : { data }) };
117
+ }
118
+ const wrapped = asRecord(op.value);
119
+ return wrapped && typeof wrapped.type === 'string' ? wrapped : { type: 'static', data: op.value };
120
+ }
121
+ /**
122
+ * Sayfanın yazımdan SONRAKİ etiketleri: kayıtlı ağaç + bu yazımın ekledikleri ve yazdıkları; kaldırılan düğümler alt
123
+ * ağaçlarıyla birlikte düşer. `written` bu yazımın dokunduğu düğümlerdir — MOB-030 yalnız onları içeren yinelemeyi
124
+ * söyler. Sıra "kayıtlılar, sonra yazılanlar"dır, belge sırası değil: yama sonrasının sırası burada bilinmez.
125
+ */
126
+ export function heroTagsAfterWrite(input) {
127
+ const written = new Map();
128
+ const removed = new Set();
129
+ const savedProps = new Map();
130
+ const savedNodes = new Map();
131
+ walkNodes(input.savedNodes, (node, elementId) => {
132
+ savedNodes.set(elementId, node);
133
+ savedProps.set(elementId, asRecord(node.props)?.heroTag);
134
+ });
135
+ const write = (node, elementId) => written.set(elementId, heroTagEntry(elementId, asRecord(node.props)?.heroTag));
136
+ walkNodes(input.nodes, write);
137
+ for (const op of flattenBatchOps(input.ops ?? [])) {
138
+ const target = typeof op.target === 'string' ? op.target : '';
139
+ if (op.op === 'add_component')
140
+ walkNodes(op.component, write);
141
+ if (op.op === 'remove_component')
142
+ walkNodes(savedNodes.get(target), (_node, elementId) => removed.add(elementId));
143
+ if (HERO_PROP_OPS.has(String(op.op)) && op.prop === 'heroTag' && target)
144
+ written.set(target, heroTagEntry(target, writtenHeroProp(op, savedProps.get(target))));
145
+ }
146
+ return {
147
+ entries: [
148
+ ...heroTags(asList(input.savedNodes)).filter(entry => !written.has(entry.elementId) && !removed.has(entry.elementId)),
149
+ ...[...written.values()].filter((entry) => entry !== null)
150
+ ],
151
+ written: new Set(written.keys())
152
+ };
153
+ }