@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.
- package/dist/knowledge/componentCatalog.js +27 -97
- package/dist/knowledge/componentEvents.js +1 -18
- package/dist/knowledge/mobileComponentCatalog.js +114 -15
- package/dist/knowledge/mobileComponentEvents.js +41 -0
- package/dist/knowledge/motionResolve.js +252 -0
- package/dist/knowledge/normalize.js +87 -59
- package/dist/knowledge/pageDiscover.js +2 -2
- package/dist/knowledge/platform/batchOps.js +70 -0
- package/dist/knowledge/platform/heroTag.js +153 -0
- package/dist/knowledge/platform/lint.js +128 -6
- package/dist/knowledge/platform/mobile.js +1 -1
- package/dist/knowledge/platform/mobileLayoutLint.js +8 -1
- package/dist/knowledge/platform/mobileRules.js +18 -0
- package/dist/knowledge/platform/pageTransition.js +57 -0
- package/dist/knowledge/platform/web.js +1 -1
- package/dist/knowledge/rules.js +52 -1
- package/dist/knowledge/styleResolveNative.js +88 -4
- package/dist/platform/capture/actions.js +4 -1
- package/dist/platform/capture/capture.js +10 -13
- package/dist/platform/capture/persistent-browser.js +130 -0
- package/dist/resources/examples/flow-list-to-form.json +8 -10
- package/dist/resources/examples/flow-load-table.json +1 -1
- package/dist/resources/examples/mobile-list-detail-form.json +4 -3
- package/dist/resources/examples/mobile-scan-code.json +285 -0
- package/dist/resources/guides/component.md +3 -3
- package/dist/resources/guides/expressions.md +2 -2
- package/dist/resources/guides/merge.md +98 -18
- package/dist/resources/guides/microservice.md +2 -2
- package/dist/resources/guides/mobile-layout.md +2 -1
- package/dist/resources/guides/mobile-publish.md +1 -0
- package/dist/resources/guides/mobile-steps.md +67 -0
- package/dist/resources/guides/mobile.md +27 -55
- package/dist/resources/guides/page.md +182 -121
- package/dist/resources/guides/translation.md +26 -0
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/ResolveMergeConflictsCommand.json +30 -1
- package/dist/tools/activities.js +3 -1
- package/dist/tools/applications.js +16 -2
- package/dist/tools/components.js +12 -6
- package/dist/tools/designSchemaPatch.js +18 -13
- package/dist/tools/layouts.js +7 -2
- package/dist/tools/merges.js +29 -9
- package/dist/tools/pages.js +129 -127
- package/dist/tools/platformInfo.js +3 -2
- package/dist/tools/profileWrite.js +2 -2
- package/dist/tools/schemaOps.js +132 -34
- package/dist/tools/screenWrite.js +1 -1
- package/dist/tools/screens.js +3 -3
- 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 {
|
|
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
|
|
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
|
|
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
|
|
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ı
|
|
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
|
-
|
|
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 ?? '?')})
|
|
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
|
-
*
|
|
2040
|
-
*
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
2043
|
-
*
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
*
|
|
2047
|
-
*
|
|
2048
|
-
*
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
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,
|
|
2074
|
+
const walk = (list, insideType) => {
|
|
2058
2075
|
for (const node of list) {
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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),
|
|
2086
|
+
walk(asObjects(nodes), undefined);
|
|
2066
2087
|
return found;
|
|
2067
2088
|
}
|
|
2068
2089
|
/**
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
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(
|
|
2073
|
-
const
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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', '
|
|
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('
|
|
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
|
+
}
|