@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
@@ -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
+ }
@@ -1,9 +1,19 @@
1
1
  import { IMAGE_SOURCE_PROPS, IMAGE_SOURCE_PROP_NAMES, MOBILE_RULES, REDIRECT_PRESENTATIONS, fileIdImageExpression, missingComponentAdvice, styleReplacement } from './mobileRules.js';
2
+ import { flattenBatchOps } from './batchOps.js';
3
+ import { heroTagProblems, heroTagsAfterWrite, writesHeroTag } from './heroTag.js';
4
+ import { PAGE_TRANSITIONS, resolveTransition } from './pageTransition.js';
2
5
  import { findingLine } from './profile.js';
3
6
  import { SURFACE_KEYS } from '../styleResolveNative.js';
7
+ import { MOTION_BINDING_TYPES, MOTION_FIELDS, motionFieldBinding, motionValue } from '../motionResolve.js';
4
8
  import { DOCUMENT_STYLE_TYPES } from '../styleResolve.js';
5
9
  import { listThemeVarNames } from '../themeResolve.js';
6
10
  import { closestArgument } from '../../platform/argumentAliases.js';
11
+ /**
12
+ * Uyarının yazımı durdurmadığını söyleyen cümle. OLAYI değil KURALI anlatır: aynı bulgu kuru koşuda
13
+ * (`page_schema_validate`) da döner ve orada hiçbir şey yazılmamıştır — "Yazım platforma gitti" kuru koşuda yanlıştı
14
+ * (TOOL-060, ölçüldü 2026-10-10, gerçek uç). Regresyon: `test/dry-run-wording.test.ts`.
15
+ */
16
+ const NOT_BLOCKING = 'Yazım reddedilmez, platforma gider.';
7
17
  function asRecord(value) {
8
18
  return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
9
19
  }
@@ -49,6 +59,7 @@ function label(node) {
49
59
  }
50
60
  const GRID_LAYOUTS = new Set(['grid', 'cssgrid']);
51
61
  const RELATIVE_UNIT = /\b\d*\.?\d+(em|rem)\b|calc\(/i;
62
+ const TEMPLATE_VALUE = /\{\{[\s\S]*\}\}/;
52
63
  /**
53
64
  * THEME-012: mobil çizici `var(--ng-…)`'yi yalnız temanın yaydığı değişkenlerden çözer
54
65
  * (`compose-native-style.ts` `resolveThemeToken`); web'deki bileşen yerel değişkenleri (`--ng-nav-bg` …)
@@ -103,6 +114,7 @@ export function profileFindings(profile, input) {
103
114
  const floorImages = [];
104
115
  const namelessIconButtons = [];
105
116
  const unknownTokens = [];
117
+ const motionSkips = [];
106
118
  const hit = (map, key, target) => map.set(key, [...(map.get(key) ?? []), target]);
107
119
  const inspectTokens = (target, value) => {
108
120
  for (const name of unknownThemeTokens(value)) {
@@ -136,6 +148,42 @@ export function profileFindings(profile, input) {
136
148
  hit(styleHits, `surfaceStyle.${key}`, target);
137
149
  }
138
150
  };
151
+ // MOB-028: hareket çözücüsü tanımadığı alt anahtarı ve okuyamadığı değeri ATLAR, nesne olmayan grubu hareket
152
+ // saymaz (frontend `resolve-motion.ts`) — yazım kabul edilir, hareket uyarısız eksik çıkar. Uyarı metni
153
+ // çözücününkidir. İfadeye bağlı GRUP (`code`/`translation` sarmalı; `data`sı yalnız tasarım yedeğidir) çalışma
154
+ // anında çözülür: burada okunamaz, denetlenmez. Grubun içindeki TEK alan ifadeye (`code`) bağlıysa o da çalışma
155
+ // anında değerlendirilir (frontend ADR 0020): uyarı değildir, yalnız tasarım yedeği denetlenir. Alanda çözülmeyen
156
+ // yazım (şablon metni, `code` dışı bağ) söylenir (`motionFieldBinding`).
157
+ const inspectMotion = (target, value) => {
158
+ if (boundWrapper(value))
159
+ return;
160
+ const raw = asRecord(value);
161
+ // Veri taşımayan sarmal yalnız BİLİNEN bir değer türüyse sarmaldır: alanı yanlışlıkla `type` diye yazılmış ham
162
+ // grup (`{ type: 'fade', duration: 300 }`) sarmal sanılıp denetimsiz geçiyordu.
163
+ if (raw && typeof raw.type === 'string' && (raw.type === 'static' || MOTION_BINDING_TYPES.includes(raw.type)) && !('data' in raw))
164
+ return;
165
+ const group = propValue(value);
166
+ if (group === undefined || group === null || group === '' || (typeof group === 'string' && TEMPLATE_VALUE.test(group)))
167
+ return;
168
+ const fields = asRecord(group);
169
+ if (!fields) {
170
+ motionSkips.push(`${target}: motionStyle: ${typeof group === 'string' ? `'${group}'` : JSON.stringify(group)} nesne değil — alanlar nesnenin içinde yazılır ({ enter: 'fade' }), hareket yok sayılır`);
171
+ return;
172
+ }
173
+ for (const [key, wrapped] of Object.entries(fields)) {
174
+ const binding = motionFieldBinding(key, wrapped);
175
+ if (binding?.kind === 'unresolved')
176
+ motionSkips.push(`${target}: ${binding.warning}`);
177
+ const field = binding ? binding.fallback : propValue(wrapped);
178
+ if (field === undefined || field === null || field === '')
179
+ continue;
180
+ const warning = MOTION_FIELDS.includes(key)
181
+ ? motionValue(key, field).warning
182
+ : `motionStyle.${key}: tanınmayan alan, atlandı`;
183
+ if (warning)
184
+ motionSkips.push(`${target}: ${warning}`);
185
+ }
186
+ };
139
187
  const inspectNode = (node) => {
140
188
  const target = label(node);
141
189
  const type = String(node.type ?? '');
@@ -154,6 +202,7 @@ export function profileFindings(profile, input) {
154
202
  gridNodes.push(`${target} (layout:${layout})`);
155
203
  inspectAppearance(target, asRecord(node.appearance));
156
204
  inspectSurface(target, props.surfaceStyle);
205
+ inspectMotion(target, props.motionStyle);
157
206
  for (const group of STYLE_GROUP_PROPS)
158
207
  inspectTokens(target, props[group]);
159
208
  if (type === 'image' && !imageHasDefiniteHeight(asRecord(node.appearance)))
@@ -182,7 +231,9 @@ export function profileFindings(profile, input) {
182
231
  walk(input.nodes, inspectNode);
183
232
  const savedTypes = new Map();
184
233
  walk(input.savedNodes, node => savedTypes.set(label(node), String(node.type ?? '')));
185
- for (const op of input.ops ?? []) {
234
+ // Toplu yamanın zarfı açılır: içindeki op'lar tek tek yazılmış gibi denetlenir (TOOL-057).
235
+ const ops = flattenBatchOps(input.ops ?? []);
236
+ for (const op of ops) {
186
237
  const target = typeof op.target === 'string' ? op.target : '?';
187
238
  if (op.op === 'add_component')
188
239
  walk(op.component, inspectNode);
@@ -203,6 +254,8 @@ export function profileFindings(profile, input) {
203
254
  hit(styleHits, 'hoverStyle', target);
204
255
  if (prop === 'surfaceStyle')
205
256
  inspectSurface(target, op.value);
257
+ if (prop === 'motionStyle')
258
+ inspectMotion(target, op.value);
206
259
  if (STYLE_GROUP_PROPS.has(prop))
207
260
  inspectTokens(target, op.value);
208
261
  // MOB-011: var olan düğümün tipi op'ta yok — tip biliniyorsa (aynı yazımda eklendi) ona, yoksa prop adına bakılır.
@@ -220,7 +273,7 @@ export function profileFindings(profile, input) {
220
273
  message: [
221
274
  blockNodes.length > 0 ? `blok (12 sütun ızgara) mobil çizerde yok sayılır: ${[...new Set(blockNodes)].join(', ')}` : '',
222
275
  gridNodes.length > 0 ? `grid/cssgrid yerleşimi yok, kap flex çizilir: ${gridNodes.join(', ')}` : ''
223
- ].filter(Boolean).join(' · ') + ". Sıra dizi indeksinden, yerleşim flex'ten gelir (direction/gap/horizontalAlign). Yazım platforma gitti.",
276
+ ].filter(Boolean).join(' · ') + ". Sıra dizi indeksinden, yerleşim flex'ten gelir (direction/gap/horizontalAlign). " + NOT_BLOCKING,
224
277
  guide: "platform_guide('mobile')"
225
278
  });
226
279
  }
@@ -229,7 +282,33 @@ export function profileFindings(profile, input) {
229
282
  code: MOBILE_RULES['MOB-002'].id,
230
283
  severity: 'warn',
231
284
  target: [...new Set(targets)].join(', '),
232
- message: `'${key}' mobilde çizilmez — ${styleReplacement(key.split(' ')[0] ?? key)}. Yazım platforma gitti.`
285
+ message: `'${key}' mobilde çizilmez — ${styleReplacement(key.split(' ')[0] ?? key)}. ${NOT_BLOCKING}`
286
+ });
287
+ }
288
+ // MOB-030: aynı sayfada aynı statik heroTag (frontend `hero-tag.ts`; ayna heroTag.ts). Sayfanın yazımdan SONRAKİ
289
+ // etiketleri okunur ve yalnız bu yazımın dokunduğu bir düğümü içeren yineleme söylenir — kayıtlı ağaçta zaten duran
290
+ // yineleme ilgisiz her yazımda yinelenmez. Bilgidir: yineleme yalnız sayfa bir yakınlaşmanın HEDEFİ olduğunda
291
+ // belirsizdir; kaynak sayfada aynı etiketli kartlar olağandır (kaynak dokunulan düğümdür).
292
+ const hero = heroTagsAfterWrite({ nodes: input.nodes, savedNodes: input.savedNodes, ops });
293
+ const heroDuplicates = [...new Set(hero.entries.filter(entry => entry.tag !== undefined).map(entry => entry.tag))]
294
+ .map(tag => hero.entries.filter(entry => entry.tag === tag))
295
+ .filter(group => group.length > 1 && group.some(entry => hero.written.has(entry.elementId)));
296
+ if (heroDuplicates.length > 0) {
297
+ findings.push({
298
+ code: MOBILE_RULES['MOB-030'].id,
299
+ severity: 'info',
300
+ target: heroDuplicates.flatMap(group => group.map(entry => entry.elementId)).join(', '),
301
+ message: `${heroDuplicates.flatMap(group => heroTagProblems(group)).join(' · ')}. Yalnız bu sayfa bir yakınlaşmanın HEDEFİ olduğunda önemlidir: hizalama kutusu tektir. Kaynak sayfada aynı etiketli kartlar ve liste satırları sorun değildir (kaynak dokunulan düğümdür); hedefte ayrı öğelerse ayrı etiket verin.`,
302
+ guide: "platform_guide('page') §Kural 1g-3"
303
+ });
304
+ }
305
+ if (motionSkips.length > 0) {
306
+ findings.push({
307
+ code: MOBILE_RULES['MOB-028'].id,
308
+ severity: 'warn',
309
+ target: [...new Set(motionSkips.map(entry => entry.split(':')[0] ?? entry))].join(', '),
310
+ message: `motionStyle'da çözücünün atladığı alan var, hareket eksik çıkar: ${[...new Set(motionSkips)].join(' · ')}. Alanlar: ${MOTION_FIELDS.join(', ')}. ${NOT_BLOCKING}`,
311
+ guide: "platform_guide('mobile')"
233
312
  });
234
313
  }
235
314
  if (unknownTokens.length > 0) {
@@ -237,7 +316,7 @@ export function profileFindings(profile, input) {
237
316
  code: 'THEME-012',
238
317
  severity: 'warn',
239
318
  target: [...new Set(unknownTokens.map(entry => entry.split(':')[0] ?? entry))].join(', '),
240
- message: `temada tanımlı olmayan jeton cihazda çözülmez (zemin saydam kalır, iOS gölgesi içeriğe düşer): ${[...new Set(unknownTokens)].join(' · ')}. Geçerli adlar temanın yaydığı değişkenlerdir (theme_get); bilerek kullanıyorsanız yedek verin: var(--ad, #fff). Yazım platforma gitti.`,
319
+ message: `temada tanımlı olmayan jeton cihazda çözülmez (zemin saydam kalır, iOS gölgesi içeriğe düşer): ${[...new Set(unknownTokens)].join(' · ')}. Geçerli adlar temanın yaydığı değişkenlerdir (theme_get); bilerek kullanıyorsanız yedek verin: var(--ad, #fff). ${NOT_BLOCKING}`,
241
320
  guide: "platform_guide('theme')"
242
321
  });
243
322
  }
@@ -291,12 +370,14 @@ export function profileFindings(profile, input) {
291
370
  code: MOBILE_RULES['MOB-011'].id,
292
371
  severity: 'warn',
293
372
  target: fileIdSources.map(source => source.target).join(', '),
294
- message: `${[...new Set(fileIdSources.map(source => `'${source.expression}'`))].join(', ')} '.value' ile bitiyor — kayıttan okunan FILE alanı { type: 'ref', value: <dosya id>, absolute_path: <url> } döner ve '.value' dosya KİMLİĞİDİR (GUID), görsel çizilmez. Nesnenin kendisini verin ('${object}') ya da '.absolute_path' yazın. Gateway alan tipini sayfa şemasından bilemez: yol FILE alanı değilse (ör. URL taşıyan metin alanı) uyarıyı yok sayın. Yazım platforma gitti.`,
373
+ message: `${[...new Set(fileIdSources.map(source => `'${source.expression}'`))].join(', ')} '.value' ile bitiyor — kayıttan okunan FILE alanı { type: 'ref', value: <dosya id>, absolute_path: <url> } döner ve '.value' dosya KİMLİĞİDİR (GUID), görsel çizilmez. Nesnenin kendisini verin ('${object}') ya da '.absolute_path' yazın. Gateway alan tipini sayfa şemasından bilemez: yol FILE alanı değilse (ör. URL taşıyan metin alanı) uyarıyı yok sayın. ${NOT_BLOCKING}`,
295
374
  guide: "platform_guide('mobile') §Dosya ve fotoğraf"
296
375
  });
297
376
  }
298
377
  const infoSteps = new Map();
299
378
  const badPresentation = new Map();
379
+ const badTransition = new Map();
380
+ const unusedTransition = new Map();
300
381
  walkSteps(input.steps, step => {
301
382
  const type = String(step.type ?? '');
302
383
  if (profile.steps.infoOnly.has(type))
@@ -307,10 +388,24 @@ export function profileFindings(profile, input) {
307
388
  return;
308
389
  const args = asRecord(step.args);
309
390
  const presentation = args?.presentation;
391
+ const stepId = String(step.stepId ?? step.name ?? '?');
392
+ // MOB-029: geçişin kararını çalışma zamanının çözücüsü verir (ayna: pageTransition.ts) — lint ikinci bir kural
393
+ // yazmaz. Alan YOKSA bulgu yok: adım uygulamanın varsayılanını kullanır. Sekme hedefi burada bilinmez
394
+ // (lint sekme ayarını görmez); o durumda da geçiş kullanılmaz ve rehber bunu söyler.
395
+ // Akış araçları bu lint'i BAŞARILI yazımdan sonra koşar ve küme dışı değeri backend 422 ile önce reddeder: uyarı
396
+ // dalı bugün yalnız backend'in doğrulamadığı bir yazım yolunda konuşur (MOB-008 ile aynı durum); bilgi dalı canlıdır.
397
+ if (args?.transition !== undefined && args.transition !== null && args.transition !== '') {
398
+ const resolved = resolveTransition({ step: args.transition, presentation }, { reduceMotion: false });
399
+ const written = typeof args.transition === 'string' ? args.transition : JSON.stringify(args.transition);
400
+ if (resolved.warnings.length > 0)
401
+ hit(badTransition, written, stepId);
402
+ else if (resolved.fixedBy === 'modal')
403
+ hit(unusedTransition, written, stepId);
404
+ }
310
405
  if (presentation === undefined || presentation === null || presentation === '')
311
406
  return;
312
407
  if (!REDIRECT_PRESENTATIONS.has(String(presentation))) {
313
- hit(badPresentation, String(presentation), String(step.stepId ?? step.name ?? '?'));
408
+ hit(badPresentation, String(presentation), stepId);
314
409
  }
315
410
  });
316
411
  for (const [value, stepIds] of badPresentation) {
@@ -322,6 +417,24 @@ export function profileFindings(profile, input) {
322
417
  guide: "platform_guide('mobile')"
323
418
  });
324
419
  }
420
+ for (const [value, stepIds] of badTransition) {
421
+ findings.push({
422
+ code: MOBILE_RULES['MOB-029'].id,
423
+ severity: 'warn',
424
+ target: stepIds.join(', '),
425
+ message: `redirect.transition '${value}' tanınmıyor — ${PAGE_TRANSITIONS.join(', ')} bekleniyor. Backend doğrulayıcısı 422 döner; alanı yazmazsanız adım uygulamanın varsayılan geçişiyle açılır.`,
426
+ guide: "platform_guide('page') §Kural 1g-2"
427
+ });
428
+ }
429
+ for (const [value, stepIds] of unusedTransition) {
430
+ findings.push({
431
+ code: MOBILE_RULES['MOB-029'].id,
432
+ severity: 'info',
433
+ target: stepIds.join(', '),
434
+ message: `redirect.transition '${value}' modal sunumla birlikte yazılmış — modal sistemin kağıt animasyonuyla açılır, geçiş kullanılmaz (hedef bir sekme sayfasıysa da kullanılmaz: sekme değişir).`,
435
+ guide: "platform_guide('page') §Kural 1g-2"
436
+ });
437
+ }
325
438
  for (const [type, stepIds] of infoSteps) {
326
439
  findings.push({
327
440
  code: MOBILE_RULES['MOB-005'].id,
@@ -345,6 +458,15 @@ function walkSteps(value, visit) {
345
458
  walkSteps(step.steps, visit);
346
459
  }
347
460
  }
461
+ /**
462
+ * Denetim bu yama için kayıtlı ağacı ister mi (`ProfileLintInput.savedNodes`): `style` yazımında hedefin tipi (belge
463
+ * CSS'i taşıyan düğüm mü — MOB-002), `heroTag` yazımında sayfanın öteki etiketleri (MOB-030). Çağıran ağacı yalnız bu
464
+ * durumda okur. Denetimle AYNI op listesine bakar (zarf açılır): 2026-10-10'da denetim zarfı açıp çağıran açmayınca
465
+ * kayıtlı `html` düğümünün belge CSS'i zarf içinde yanlış MOB-002 aldı (regresyon `test/batch-profile-lint.test.ts`).
466
+ */
467
+ export function profileLintReadsSavedNodes(ops) {
468
+ return writesHeroTag(ops) || flattenBatchOps(ops).some(op => (op.op === 'replace_prop' || op.op === 'set_binding') && op.prop === 'style');
469
+ }
348
470
  /** Bulguları `layoutWarnings[]` satırlarına çevirir. */
349
471
  export function findingLines(findings) {
350
472
  return findings.map(findingLine);
@@ -116,7 +116,7 @@ export const mobileProfile = Object.freeze({
116
116
  },
117
117
  style: { freeStyle: false, hoverStyle: false, resolver: 'native' },
118
118
  steps: {
119
- registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'navigate', 'storage', 'setcookie', 'download', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show']),
119
+ registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'navigate', 'storage', 'setcookie', 'download', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show', 'back', 'scan-code']),
120
120
  aliases: STEP_ALIASES,
121
121
  infoOnly: new Set(['download', 'setcookie'])
122
122
  },
@@ -1,4 +1,4 @@
1
- import { MOBILE_DROPPABLE_TYPES, MOBILE_OUT_OF_FLOW_TYPES, MOBILE_TRANSPARENT_WRAPPER_TYPES } from '../mobileComponentCatalog.js';
1
+ import { MOBILE_DROPPABLE_TYPES, MOBILE_OPTIONAL_VIEW_INDEX, MOBILE_OUT_OF_FLOW_TYPES, MOBILE_TRANSPARENT_WRAPPER_TYPES } from '../mobileComponentCatalog.js';
2
2
  import { PAGE_CONTAINER_ELEMENT_ID } from '../normalize.js';
3
3
  import { OVERLAY_TYPES, firstDrawn, safeAreaEdges, topSafeAreaMissing } from './mobile.js';
4
4
  import { MOBILE_RULES } from './mobileRules.js';
@@ -82,6 +82,8 @@ export function mobileLayoutFindings(input) {
82
82
  return;
83
83
  if (type === 'safe-area' && ancestors.length === 0)
84
84
  return;
85
+ if (isOptionalView(node, ancestors.at(-1)))
86
+ return;
85
87
  findings.push({ ...rule('MOB-018', label(node), `boş ${type}: ekranda boş şerit bırakır${type === 'safe-area' ? ` (edges: '${safeAreaEdges(node)}' payı kadar)` : ''}. İçeriğini ekleyin ya da silin`), severity: 'info' });
86
88
  });
87
89
  if (screenRoot)
@@ -282,6 +284,11 @@ function key(finding) {
282
284
  function rule(code, target, message) {
283
285
  return { code: MOBILE_RULES[code].id, severity: MOBILE_RULES[code].severity, target, message, guide: "platform_guide('mobile-layout')" };
284
286
  }
287
+ /** Sabit görünümlü sahibinin bilerek boş bırakılabilen görünümü: veri yokken açılan görünüm (`MOBILE_OPTIONAL_VIEW_INDEX`). */
288
+ function isOptionalView(node, parent) {
289
+ const index = MOBILE_OPTIONAL_VIEW_INDEX[String(parent?.type ?? '')];
290
+ return index !== undefined && asNodes(parent?.children).filter(child => child.type === 'view')[index] === node;
291
+ }
285
292
  function asNodes(value) {
286
293
  return Array.isArray(value) ? value.filter((item) => !!item && typeof item === 'object') : [];
287
294
  }
@@ -114,6 +114,24 @@ export const MOBILE_RULES = {
114
114
  severity: 'info',
115
115
  sourceRef: 'zeyrek-mcp guides/mobile-layout.md §Sözleşme S5/S7/S11',
116
116
  summary: 'Ölçek dışı değer: özel boşluk ya da sayısal gap 0/4/8/16/24/32 dışında, ham yarıçap, px yazı boyu'
117
+ },
118
+ 'MOB-028': {
119
+ id: 'MOB-028',
120
+ severity: 'warn',
121
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/resolve-motion.ts (tanınmayan alan ve okunamayan değer atlanır)',
122
+ summary: "Mobil motionStyle'da tanınmayan alt anahtar ya da küme/aralık dışı değer — çözücü atlar, hareket eksik çıkar"
123
+ },
124
+ 'MOB-029': {
125
+ id: 'MOB-029',
126
+ severity: 'warn',
127
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/resolve-transition.ts (çözücü; ayna pageTransition.ts); zeyrek-backend FlowDtoValidator (StepTransition); simülatör ölçümü 2026-10-10',
128
+ summary: "redirect.transition ya da properties.mobile.transition kümede değil (default | none | fade | slide | slideUp) — adımda backend 422; modal sunumda yazılan geçiş kullanılmaz (bilgi)"
129
+ },
130
+ 'MOB-030': {
131
+ id: 'MOB-030',
132
+ severity: 'info',
133
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/hero-tag.ts (kural; ayna heroTag.ts); zeyrek-frontend/docs/adr/0022-hero-yolu.md; simülatör ölçümü 2026-10-10',
134
+ summary: "Aynı sayfada aynı statik heroTag'i birden çok düğüm taşıyor — sayfa yakınlaşmanın hedefi olduğunda ilk çizilen hizalanır; kaynak sayfada sorun değildir"
117
135
  }
118
136
  };
119
137
  /**
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Mobil sayfa geçişinin gateway aynası: sözlük ve çözücü. Kaynak frontend'dedir
3
+ * (`zeyrek-frontend/packages/ng-apps-ui-native/src/motion/` — `transition-fields.ts`, `resolve-transition.ts`;
4
+ * uygulama ayarının okuyucusu `src/shell/mobile-transition.ts`). Bu dosya onun kopyasıdır ve
5
+ * `test/page-transition-parity.test.ts` ile kilitlidir: frontend'in altın vakaları (`transition-golden.json`) aynı
6
+ * girdiden aynı çıktıyı ve harfi harfine aynı uyarıyı vermeli; aynı dosyanın `values` listesi backend enum'unun
7
+ * (`StepTransition`) kopyasına da kilitlidir. Test kırılınca test düzeltilmez, bu dosya senkronlanır.
8
+ *
9
+ * Değerler ürünün dilidir; yerel yığının adları (`slide_from_right`) burada geçmez. Kuralların ölçümü:
10
+ * simülatör, 2026-10-10 (`zeyrek-frontend/docs/features/mobile-runtime.md` §Sayfa geçişi).
11
+ */
12
+ export const PAGE_TRANSITIONS = ['default', 'none', 'fade', 'slide', 'slideUp'];
13
+ /** Boş değer ne değer ne uyarıdır (seçilmemiş); kümede olmayan değer `value` taşımaz. */
14
+ export function transitionValue(raw, source) {
15
+ if (raw === undefined || raw === null || (typeof raw === 'string' && raw.trim() === ''))
16
+ return {};
17
+ if (typeof raw === 'string' && PAGE_TRANSITIONS.includes(raw))
18
+ return { value: raw };
19
+ return {
20
+ warning: `${source}: ${typeof raw === 'string' ? `'${raw}'` : JSON.stringify(raw)} geçersiz — ${PAGE_TRANSITIONS.join(', ')}, atlandı`
21
+ };
22
+ }
23
+ export const TRANSITION_SOURCES = { step: 'redirect.transition', application: 'properties.mobile.transition' };
24
+ /**
25
+ * "Bu gezinmede hangi geçiş oynar." Öncelik: adım → uygulama varsayılanı → `default`; okunamayan değer uyarısıyla
26
+ * atlanır. Sekme hedefi ve `modal` sunum seçimi kullanmaz (sekme önce gelir). "Hareketi azalt"ta seçilmiş geçiş
27
+ * `none` olur, `default` sisteme kalır.
28
+ */
29
+ export function resolveTransition(input, context) {
30
+ const step = transitionValue(input.step, TRANSITION_SOURCES.step);
31
+ const application = transitionValue(input.application, TRANSITION_SOURCES.application);
32
+ const warnings = [step.warning, application.warning].filter((warning) => warning !== undefined);
33
+ if (input.tabTarget)
34
+ return { transition: 'default', fixedBy: 'tab', warnings };
35
+ if (input.presentation === 'modal')
36
+ return { transition: 'default', fixedBy: 'modal', warnings };
37
+ const chosen = step.value ?? application.value ?? 'default';
38
+ return { transition: context.reduceMotion && chosen !== 'default' ? 'none' : chosen, warnings };
39
+ }
40
+ /**
41
+ * `properties.mobile.transition`'ın HAM değeri; alan yoksa `undefined`. Frontend'in `readMobileTransition`'ı doğrulanmış
42
+ * değeri verir ve bozuk olanı sessizce düşürür; gateway uyarmak için ham değeri ister — adı bu yüzden farklı.
43
+ */
44
+ export function rawMobileTransition(properties) {
45
+ const mobile = properties?.mobile;
46
+ return mobile && typeof mobile === 'object' ? mobile.transition : undefined;
47
+ }
48
+ /**
49
+ * MOB-029'un uygulama ayarı ayağı. Backend `properties`'i doğrulamaz (jsonb serbest) ve cihazın okuyucusu sözlük
50
+ * dışı değeri SESSİZCE düşürür — uyarının verildiği tek yer burasıdır.
51
+ */
52
+ export function mobileTransitionProblems(properties) {
53
+ const { warning } = transitionValue(rawMobileTransition(properties), TRANSITION_SOURCES.application);
54
+ return warning
55
+ ? [`${warning} — cihaz değeri okumaz, geçişini seçmemiş yönlendirme adımları sistemin geçişiyle açılır`]
56
+ : [];
57
+ }
@@ -20,7 +20,7 @@ export const webProfile = Object.freeze({
20
20
  },
21
21
  style: { freeStyle: true, hoverStyle: true, resolver: 'css' },
22
22
  steps: {
23
- registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show']),
23
+ registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show', 'back', 'scan-code']),
24
24
  aliases: STEP_ALIASES,
25
25
  infoOnly: new Set()
26
26
  },