@appaflytech/zeyrek-ai-mcp 0.0.8 → 0.0.9

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 (39) hide show
  1. package/README.md +27 -21
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/componentCatalog.js +122 -27
  4. package/dist/knowledge/componentEvents.js +35 -4
  5. package/dist/knowledge/errorPath.js +1 -1
  6. package/dist/knowledge/normalize.js +15 -0
  7. package/dist/knowledge/rules.js +3 -1
  8. package/dist/knowledge/styleResolve.js +46 -4
  9. package/dist/knowledge/themeResolve.js +936 -0
  10. package/dist/knowledge/themeRules.js +221 -0
  11. package/dist/knowledge/themeSchema.js +335 -0
  12. package/dist/platform/domSummary.js +144 -0
  13. package/dist/platform/screenshot.js +414 -23
  14. package/dist/platform/screenshotMemory.js +55 -0
  15. package/dist/resources/examples/layout-app-shell.json +1 -1
  16. package/dist/resources/examples/theme-brand.json +162 -0
  17. package/dist/resources/guides/component.md +7 -1
  18. package/dist/resources/guides/getting-started.md +4 -0
  19. package/dist/resources/guides/layout.md +30 -26
  20. package/dist/resources/guides/microservice.md +55 -1
  21. package/dist/resources/guides/page.md +87 -44
  22. package/dist/resources/guides/theme.md +210 -0
  23. package/dist/resources/guides/translation.md +6 -4
  24. package/dist/resources/guides/workflow.md +91 -6
  25. package/dist/resources/schema-fingerprint.json +2 -2
  26. package/dist/server.js +52 -43
  27. package/dist/tools/components.js +8 -0
  28. package/dist/tools/designSchemaPatch.js +152 -0
  29. package/dist/tools/flows.js +15 -18
  30. package/dist/tools/layouts.js +4 -4
  31. package/dist/tools/microservices.js +5 -5
  32. package/dist/tools/pages.js +196 -323
  33. package/dist/tools/projects.js +66 -9
  34. package/dist/tools/schemaOps.js +280 -0
  35. package/dist/tools/screenWrite.js +2 -1
  36. package/dist/tools/screens.js +0 -7
  37. package/dist/tools/themes.js +163 -0
  38. package/dist/tools/workflows.js +1 -1
  39. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { PlatformApiError } from './errors.js';
2
+ import { parseDomSummary } from './domSummary.js';
2
3
  import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../knowledge/componentCatalog.js';
3
4
  import { logStep } from '../observability/trace.js';
4
5
  /**
@@ -29,10 +30,145 @@ export function actionScript(action) {
29
30
  var node = document.querySelector(${selector});
30
31
  if (!node) return { found: false, applied: false, requests: count };
31
32
  var target = node.matches('button, a, [role="button"]') ? node : (node.querySelector('button, a, [role="button"]') || node);
33
+ // S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
34
+ // ajan ikinci sekmeyi hiç seçemiyordu (2026-09-16 raporu madde 6). Sekme seçmek 'tab'in işi.
35
+ if (target.closest && target.closest('[role="tablist"]') && !node.matches('[role="tab"]')) {
36
+ return { found: true, applied: false, note: "hedef bir sekme grubu: click İLK sekmeye giderdi — sekme seçmek için { action: 'tab', target: '" + ${JSON.stringify(String(action.target ?? ''))} + "', value: '<sekme anahtarı ya da metni>' } kullanın", requests: count };
37
+ }
32
38
  target.click();
33
39
  return { found: true, applied: true, requests: count };
34
40
  })()`;
35
41
  }
42
+ // S-2 — sekme SEÇİLEMİYORDU: kimlik `<elementId>__tab-<key>` (frontend f-02) önce denenir,
43
+ // runtime onu basmıyorsa görünen metinle eşlenir ve rapor hangisiyle eşlediğini söyler —
44
+ // metin çeviriye bağlıdır, dil değişince kırılır.
45
+ if (action.action === 'tab') {
46
+ const roleSelector = JSON.stringify(`[data-element-id="${String(action.target ?? '')}__tab-${String(action.value ?? '')}"]`);
47
+ return `(function () {
48
+ var count = performance.getEntriesByType('resource').length;
49
+ var root = document.querySelector(${selector});
50
+ if (!root) return { found: false, applied: false, requests: count };
51
+ var wanted = String(${value});
52
+ var byRole = document.querySelector(${roleSelector});
53
+ var buttons = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
54
+ var byText = buttons.filter(function (button) { return (button.textContent || '').trim() === wanted; })[0]
55
+ || buttons.filter(function (button) { return (button.textContent || '').trim().toLowerCase() === wanted.toLowerCase(); })[0];
56
+ var target = byRole || byText;
57
+
58
+ if (!target) {
59
+ var names = buttons.map(function (button) { return (button.textContent || '').trim(); }).join(' · ');
60
+ return { found: true, applied: false, optionsSeen: buttons.length, requests: count, note: buttons.length === 0
61
+ ? "bu düğümde sekme yok (role=tab) — hedef gerçekten bir sekme grubu mu? page_schema_get ile tipe bakın"
62
+ : "sekme bulunamadı: " + wanted + " — sekme ANAHTARINI ya da görünen metnini verin; görülenler: " + names };
63
+ }
64
+
65
+ if (target.disabled === true || target.getAttribute('aria-disabled') === 'true') {
66
+ return { found: true, applied: false, note: "sekme devre dışı (disabled): " + wanted, requests: count };
67
+ }
68
+
69
+ target.click();
70
+ return { found: true, applied: true, matchedBy: byRole ? 'role' : 'text', requests: count };
71
+ })()`;
72
+ }
73
+ // S-3 — açılır kutuda seçim. 'set' ile aynı gövde; buradaki fark yalnız prop izi tutulmamasıdır
74
+ // (seçim kutunun kendi değerini yazar, DOM'a elle yazılan bir değer yoktur).
75
+ if (action.action === 'select') {
76
+ return `(function () {
77
+ var count = performance.getEntriesByType('resource').length;
78
+ var node = document.querySelector(${selector});
79
+ if (!node) return { found: false, applied: false, requests: count };
80
+ var wanted = String(${value});
81
+ var finish = function (result) { return result; };
82
+
83
+ var combobox = node.querySelector('[role="combobox"], .ng-select__value-container, .ng-multiselect__value-container');
84
+ if (combobox) {
85
+ // Liste BU kutunun kabının içindedir. Sorgular belge kapsamlı kaldığında açık duran başka
86
+ // bir kutunun (multiselect seçimden sonra kapanmıyor) seçeneğine tıklanabiliyordu ve adım
87
+ // "uygulandı" diyordu; komşunun yükleme göstergesi de bu kutunun kararını blokluyordu (B-5).
88
+ var scope = combobox.closest('.ng-select, .ng-multiselect') || node;
89
+
90
+ combobox.click();
91
+
92
+ return new Promise(function (resolve) {
93
+ var waited = 0;
94
+ var step = 100;
95
+ var limit = 3000;
96
+
97
+ var attempt = function () {
98
+ var loading = scope.querySelector('.ng-select__loading, .ng-multiselect__loading');
99
+ var options = Array.prototype.slice.call(scope.querySelectorAll('[role="treeitem"][data-value]'));
100
+ var byValue = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0];
101
+ var byText = options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
102
+ var match = byValue || byText;
103
+
104
+ if (match) {
105
+ var button = match.querySelector('button');
106
+ // Seçilemeyen seçeneğe tıklamak hiçbir şey yapmaz; "uygulandı" demek yanlış olurdu (B-6).
107
+ if (match.getAttribute('aria-disabled') === 'true' || (button && button.disabled)) {
108
+ resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: "seçenek devre dışı (aria-disabled): " + wanted }));
109
+ return;
110
+ }
111
+
112
+ (button || match).click();
113
+ resolve(finish({ found: true, applied: true, matchedBy: byValue ? 'value' : 'text', optionsSeen: options.length, waitedMs: waited, requests: count }));
114
+ return;
115
+ }
116
+
117
+ var settled = !loading && (options.length > 0 || !!scope.querySelector('.ng-select__empty, .ng-multiselect__empty'));
118
+
119
+ if (settled || waited >= limit) {
120
+ var seen = options.slice(0, 10).map(function (option) { return (option.textContent || '').trim(); }).join(' · ');
121
+ // Liste HİÇ açılmadıysa kusur sorguda değil kutunun kendisinde olabilir:
122
+ // handleContainerClick kilitli/salt-okunur/yükleniyor kutuda erken döner.
123
+ var locked = !scope.querySelector('.ng-select__list, .ng-multiselect__list');
124
+ var note = options.length > 0
125
+ ? "açılır kutuda bu değere karşılık gelen seçenek yok: " + wanted + " — görülen " + options.length + " seçenek: " + seen
126
+ : locked
127
+ ? "açılır kutu AÇILMADI (" + waited + " ms): kutu devre dışı, salt okunur ya da hâlâ yükleniyor olabilir — page_runtime_get ile isDisabled/isReadonly/isLoading prop'larına bakın"
128
+ : "açılır kutu açıldı ama " + waited + " ms içinde hiç seçenek basılmadı — seçenekler bir sorgudan geliyorsa query_preview ile doğrulayın, yavaşsa önce bir wait adımı ekleyin";
129
+ resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: note }));
130
+ return;
131
+ }
132
+
133
+ waited += step;
134
+ setTimeout(attempt, step);
135
+ };
136
+
137
+ attempt();
138
+ });
139
+ }
140
+ return { found: true, applied: false, note: "bu düğümde açılır kutu yok (role=combobox / .ng-select__value-container) — native alan için 'set', butona basmak için 'click' kullanın", requests: count };
141
+ })()`;
142
+ }
143
+ // S-4 — form gönderme. Gönder butonu olmayan ya da Enter ile gönderilen form başsız
144
+ // doğrulanamıyordu; `requestSubmit` formun kendi submit olayını doğduğu gibi doğurur.
145
+ if (action.action === 'submit') {
146
+ return `(function () {
147
+ var count = performance.getEntriesByType('resource').length;
148
+ var node = document.querySelector(${selector});
149
+ if (!node) return { found: false, applied: false, requests: count };
150
+ var form = node.matches('form') ? node : (node.querySelector('form') || node.closest('form'));
151
+
152
+ if (form) {
153
+ if (typeof form.requestSubmit === 'function') {
154
+ form.requestSubmit();
155
+ return { found: true, applied: true, submitVia: 'requestSubmit', requests: count };
156
+ }
157
+
158
+ form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
159
+ return { found: true, applied: true, submitVia: 'dispatch', requests: count };
160
+ }
161
+
162
+ var button = node.querySelector('button[type="submit"]');
163
+
164
+ if (button) {
165
+ button.click();
166
+ return { found: true, applied: true, submitVia: 'button', requests: count };
167
+ }
168
+
169
+ return { found: true, applied: false, note: "bu düğüm bir form değil ve içinde submit butonu da yok — form komponentinin elementId'sini hedefleyin", requests: count };
170
+ })()`;
171
+ }
36
172
  // G-09 — kanban kartı ve takvim etkinliği HTML5 DnD ile taşınır. Olaylar sentetik atılır ve
37
173
  // TEK DataTransfer'i paylaşır; koordinat bırakma hedefinin merkezidir, çünkü takvimin saat
38
174
  // kolonu bırakılan saati `clientY`'den hesaplar (slot hedefi o saatin ortasına düşer).
@@ -82,28 +218,65 @@ export function actionScript(action) {
82
218
  if (parts.length > 1) return finish({ found: true, applied: false, note: "çok alanlı bileşen: kökte tek değer yok, değeri kutulardan birine yazın — " + parts.join(' · '), requests: count });
83
219
  }
84
220
 
85
- // Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak
86
- // hiçbir şey yapmaz, seçeneğe TIKLANMASI gerekir. Liste ancak kutu açıkken
87
- // basılır, o yüzden önce kutu açılır, sonra data-value ile seçenek bulunur
88
- // (bulunamazsa görünen metinle eşlenir).
221
+ // Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak hiçbir şey
222
+ // yapmaz, seçeneğe TIKLANMASI gerekir. 'set' bu noktada 'select' fiiliyle AYNI yola girer —
223
+ // iki kopya seçim mantığı tutulmaz.
224
+
89
225
  var combobox = node.querySelector('[role="combobox"], .ng-select__value-container, .ng-multiselect__value-container');
90
226
  if (combobox) {
227
+ // Liste BU kutunun kabının içindedir. Sorgular belge kapsamlı kaldığında açık duran başka
228
+ // bir kutunun (multiselect seçimden sonra kapanmıyor) seçeneğine tıklanabiliyordu ve adım
229
+ // "uygulandı" diyordu; komşunun yükleme göstergesi de bu kutunun kararını blokluyordu (B-5).
230
+ var scope = combobox.closest('.ng-select, .ng-multiselect') || node;
231
+
91
232
  combobox.click();
92
233
 
93
- // Liste tıklamadan SONRA basılır; aynı tick'te sorgulamak boş sonuç verir.
94
- // evaluate döndürülen promise'i bekler, o yüzden seçim bir sonraki tur yapılır.
95
234
  return new Promise(function (resolve) {
96
- setTimeout(function () {
97
- var options = Array.prototype.slice.call(document.querySelectorAll('[role="treeitem"][data-value]'));
98
- var match = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0]
99
- || options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
100
- if (!match) {
101
- resolve(finish({ found: true, applied: false, note: "açılır kutu açıldı ama bu değere karşılık gelen seçenek yok: " + wanted + " — options listesini page_runtime_get ile doğrulayın; seçenek sayısı: " + options.length, requests: count }));
235
+ var waited = 0;
236
+ var step = 100;
237
+ var limit = 3000;
238
+
239
+ var attempt = function () {
240
+ var loading = scope.querySelector('.ng-select__loading, .ng-multiselect__loading');
241
+ var options = Array.prototype.slice.call(scope.querySelectorAll('[role="treeitem"][data-value]'));
242
+ var byValue = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0];
243
+ var byText = options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
244
+ var match = byValue || byText;
245
+
246
+ if (match) {
247
+ var button = match.querySelector('button');
248
+ // Seçilemeyen seçeneğe tıklamak hiçbir şey yapmaz; "uygulandı" demek yanlış olurdu (B-6).
249
+ if (match.getAttribute('aria-disabled') === 'true' || (button && button.disabled)) {
250
+ resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: "seçenek devre dışı (aria-disabled): " + wanted }));
251
+ return;
252
+ }
253
+
254
+ (button || match).click();
255
+ resolve(finish({ found: true, applied: true, matchedBy: byValue ? 'value' : 'text', optionsSeen: options.length, waitedMs: waited, requests: count }));
256
+ return;
257
+ }
258
+
259
+ var settled = !loading && (options.length > 0 || !!scope.querySelector('.ng-select__empty, .ng-multiselect__empty'));
260
+
261
+ if (settled || waited >= limit) {
262
+ var seen = options.slice(0, 10).map(function (option) { return (option.textContent || '').trim(); }).join(' · ');
263
+ // Liste HİÇ açılmadıysa kusur sorguda değil kutunun kendisinde olabilir:
264
+ // handleContainerClick kilitli/salt-okunur/yükleniyor kutuda erken döner.
265
+ var locked = !scope.querySelector('.ng-select__list, .ng-multiselect__list');
266
+ var note = options.length > 0
267
+ ? "açılır kutuda bu değere karşılık gelen seçenek yok: " + wanted + " — görülen " + options.length + " seçenek: " + seen
268
+ : locked
269
+ ? "açılır kutu AÇILMADI (" + waited + " ms): kutu devre dışı, salt okunur ya da hâlâ yükleniyor olabilir — page_runtime_get ile isDisabled/isReadonly/isLoading prop'larına bakın"
270
+ : "açılır kutu açıldı ama " + waited + " ms içinde hiç seçenek basılmadı — seçenekler bir sorgudan geliyorsa query_preview ile doğrulayın, yavaşsa önce bir wait adımı ekleyin";
271
+ resolve(finish({ found: true, applied: false, optionsSeen: options.length, waitedMs: waited, requests: count, note: note }));
102
272
  return;
103
273
  }
104
- (match.querySelector('button') || match).click();
105
- resolve(finish({ found: true, applied: true, requests: count }));
106
- }, 300);
274
+
275
+ waited += step;
276
+ setTimeout(attempt, step);
277
+ };
278
+
279
+ attempt();
107
280
  });
108
281
  }
109
282
 
@@ -140,6 +313,19 @@ export function runtimePropsScript(elementId) {
140
313
  try { var props = read(${JSON.stringify(elementId)}); return props === null ? null : JSON.stringify(props); } catch (error) { return undefined; }
141
314
  })()`;
142
315
  }
316
+ /**
317
+ * Hangi sekme AÇIK? `tab` adımı tıklamanın işe yarayıp yaramadığını ancak bununla kanıtlar:
318
+ * tıklama "uygulandı" dese de bileşen seçimi kendi `selected` prop'una yazmamış olabilir.
319
+ * `undefined` düğüm yok, `null` sekme grubu var ama hiçbiri seçili değil.
320
+ */
321
+ export function activeTabScript(elementId) {
322
+ return `(function () { /* activeTabSnapshot */
323
+ var root = document.querySelector(${JSON.stringify(`[data-element-id="${elementId}"]`)});
324
+ if (!root) return undefined;
325
+ var active = root.querySelector('[role="tab"][aria-selected="true"]');
326
+ return active ? String(active.textContent || '').replace(/\\s+/g, ' ').trim() : null;
327
+ })()`;
328
+ }
143
329
  /**
144
330
  * fullPage karesi Chromium'un viewport ötesi yakalamasıyla değil, viewport'u içerik boyuna
145
331
  * büyüterek alınır: Studio runtime'ı kendi kaydırma kabını `100vh` üzerine kuruyor ve viewport
@@ -160,6 +346,123 @@ export const contentHeightScript = [
160
346
  ' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
161
347
  '})()'
162
348
  ].join('\n');
349
+ /**
350
+ * Kırpma kutusu CSS pikselinde okunur; kare çekilirken Chromium onu `scale` ile çarpar
351
+ * (ölçüldü 2026-09-16: viewport 1280x800 · scale 0.5 · clip 300x100 → 150x50 raster).
352
+ * `null` düğüm yok demektir — tool bunu HATAYA çevirir, boş kare döndürmez.
353
+ */
354
+ export function clipRectScript(elementId) {
355
+ return `(function () { /* clipRect */
356
+ var node = document.querySelector(${JSON.stringify(`[data-element-id="${elementId}"]`)});
357
+ if (!node) return null;
358
+ var rect = node.getBoundingClientRect();
359
+ var x = rect.x + (window.scrollX || 0);
360
+ var y = rect.y + (window.scrollY || 0);
361
+ // Negatif kenar kırpılırken BOY da kısalır; yoksa kutu aşağı kayar ve kare düğümün altını gösterir.
362
+ return { x: Math.max(x, 0), y: Math.max(y, 0), width: rect.width + Math.min(x, 0), height: rect.height + Math.min(y, 0) };
363
+ })()`;
364
+ }
365
+ /**
366
+ * Karenin raster boyu başlıktan okunur: PNG'de IHDR, JPEG'de ilk SOF işaretçisi. Ölçeğin
367
+ * gerçekten uygulandığı yanıtta SAYIYLA görünsün diye var — "yarı çözünürlük istedim" ile
368
+ * "yarı çözünürlük aldım" arasındaki farkı başka hiçbir alan söylemiyor.
369
+ */
370
+ export function imageSize(buffer) {
371
+ if (buffer.length > 24 && buffer.subarray(1, 4).toString() === 'PNG') {
372
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
373
+ }
374
+ if (buffer.length > 4 && buffer[0] === 0xff && buffer[1] === 0xd8) {
375
+ let offset = 2;
376
+ while (offset + 9 < buffer.length) {
377
+ if (buffer[offset] !== 0xff) {
378
+ offset += 1;
379
+ continue;
380
+ }
381
+ const marker = buffer[offset + 1] ?? 0;
382
+ const length = buffer.readUInt16BE(offset + 2);
383
+ // SOF0-SOF3 · SOF5-SOF7 · SOF9-SOF11 · SOF13-SOF15 boyutu taşır; DHT/DRI gibi işaretçiler atlanır.
384
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
385
+ return { height: buffer.readUInt16BE(offset + 5), width: buffer.readUInt16BE(offset + 7) };
386
+ }
387
+ offset += 2 + length;
388
+ }
389
+ }
390
+ return undefined;
391
+ }
392
+ /**
393
+ * NE GÖRÜNÜYOR — resimsiz (S-5). Aynı sayfa yüklemesinden çıkan üçüncü çıktı: her KÖK runtime
394
+ * düğümünün görünürlüğü, metni, satır sayısı, açık sekmesi, değeri ve hata metinleri.
395
+ *
396
+ * Kök düğüm = kimliğinde `__` OLMAYAN düğüm; iç roller (`tbl__row-3`) satır sayısına dönüşür,
397
+ * listeye girmez. Tavan 300 düğüm, metin 80 karakter (K3) — özet bir kareyi değiştirmeli, onun
398
+ * kadar pahalı olmamalı. Tip `data-component-type`'tan gelir (frontend f-02); runtime basmıyorsa
399
+ * alan YAZILMAZ ve `typed: false` döner — `layoutReportUnavailable` ile aynı dürüstlük kuralı.
400
+ */
401
+ export function domSummaryScript(viewportHeight) {
402
+ return `(function () { /* domSummary */
403
+ var MAX = 300;
404
+ var CUT = 80;
405
+ // Görünür alanın GERÇEK boyu: fullPage ve clip yolları viewport'u içerik boyuna büyütür ve
406
+ // o anda window.innerHeight = içerik yüksekliği olur — her düğüm "ekran içinde" görünürdü ve
407
+ // "kart katlanmanın altında mı" sorusu HER ZAMAN 'visible' cevabı alırdı (B-10).
408
+ var VH = ${Math.max(Math.round(viewportHeight), 1)};
409
+ var clean = function (value) { return String(value || '').replace(/\\s+/g, ' ').trim(); };
410
+ var ROOT_SELECTOR = '[data-element-id]:not([data-element-id*="__"])';
411
+ var all = Array.prototype.slice.call(document.querySelectorAll('[data-element-id]'));
412
+ var roots = all.filter(function (node) { return String(node.getAttribute('data-element-id') || '').indexOf('__') < 0; });
413
+ var typed = false;
414
+ var nodes = roots.slice(0, MAX).map(function (node) {
415
+ var id = node.getAttribute('data-element-id');
416
+ var type = node.getAttribute('data-component-type');
417
+ if (type) typed = true;
418
+ // Bir olgu, ONU TAŞIYAN düğüme yazılır: en yakın kök ata bu düğüm değilse o olgu
419
+ // çocuğundur ve orada zaten listelenecektir (B-8). Aksi hâlde aynı değer üç satırda çıkar.
420
+ var owned = function (element) { return !!element && element.closest(ROOT_SELECTOR) === node; };
421
+ var rect = node.getBoundingClientRect();
422
+ var visible = typeof node.checkVisibility === 'function'
423
+ ? !!node.checkVisibility({ checkVisibilityCSS: true, checkOpacity: true })
424
+ : (rect.width > 0 && rect.height > 0);
425
+ var inViewport = rect.bottom > 0 && rect.right > 0 && rect.top < VH && rect.left < (window.innerWidth || 0);
426
+ // Kap düğümün innerText'i ÇOCUKLARININ metnidir; metin yalnız YAPRAK köklerde yazılır.
427
+ var nested = Array.prototype.slice.call(node.querySelectorAll('[data-element-id]'))
428
+ .some(function (child) { return String(child.getAttribute('data-element-id') || '').indexOf('__') < 0; });
429
+ var text = nested ? '' : clean(node.innerText);
430
+ if (text.length > CUT) text = text.slice(0, CUT) + '…';
431
+ var rows = document.querySelectorAll('[data-element-id^="' + id + '__row-"]').length;
432
+ var tab = node.querySelector('[role="tab"][aria-selected="true"]');
433
+ var field = node.querySelector('input, select, textarea');
434
+ var chosen = node.querySelector('.selected-item');
435
+ var holder = field || chosen;
436
+ var value = !owned(holder) ? '' : (field ? (field.type === 'password' ? '***' : clean(field.value)) : clean(chosen.textContent));
437
+ var media = node.querySelector('img, svg, canvas, video, iframe');
438
+ // İç içe <li> hem grubu hem alt mesajları sayıyordu: tek grup + 2 alt hata 3 görünüyordu (B-7).
439
+ var errorItems = Array.prototype.slice.call(node.querySelectorAll('.form-validation-errors li:not(:has(li))'))
440
+ .filter(owned).map(function (item) { return clean(item.textContent); });
441
+ // aria-invalid hem köke hem input'a yazılabiliyor (Password.svelte); yalnız ALANLAR sayılır.
442
+ var invalid = Array.prototype.slice.call(node.querySelectorAll('input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"]')).filter(owned).length;
443
+ var summary = { elementId: id, visible: !!visible, inViewport: !!inViewport };
444
+ if (type) summary.type = type;
445
+ if (text) summary.text = text;
446
+ if (rows > 0) summary.rows = rows;
447
+ if (owned(tab)) summary.activeTab = clean(tab.textContent);
448
+ if (value) summary.value = value;
449
+ if (visible && !text && !media && !nested) summary.empty = true;
450
+ if (errorItems.length > 0) summary.errors = errorItems;
451
+ else if (invalid > 0) summary.errors = ['aria-invalid: ' + invalid];
452
+ return summary;
453
+ });
454
+ var messages = [];
455
+ Array.prototype.slice.call(document.querySelectorAll('.ng-toast')).forEach(function (toast) {
456
+ var name = String(toast.className || '');
457
+ var kind = name.indexOf('ng-toast--error') >= 0 ? 'toast/error' : (name.indexOf('ng-toast--success') >= 0 ? 'toast/success' : 'toast');
458
+ messages.push({ kind: kind, text: clean(toast.innerText).slice(0, 160) });
459
+ });
460
+ Array.prototype.slice.call(document.querySelectorAll('.view-detail__error, .error-page')).forEach(function (node) {
461
+ messages.push({ kind: 'error', text: clean(node.innerText).slice(0, 160) });
462
+ });
463
+ return { nodes: nodes, messages: messages, truncated: roots.length > MAX, typed: typed };
464
+ })()`;
465
+ }
163
466
  /**
164
467
  * Kare taşmayı gösterir ama ölçmez: aynı sayfa yüklemesinden neredeyse bedavaya çıkan ikinci
165
468
  * çıktı her runtime düğümünün efektif piksel kutusudur. Düğümler `data-element-id` taşımıyorsa
@@ -261,6 +564,8 @@ const redirectQuietPolls = 12;
261
564
  const idleCloseMs = 60000;
262
565
  /** Etkileşim adımındaki `wait` üst sınırı; delayMs tavanıyla aynı çizgide. */
263
566
  const maxActionWaitMs = 15000;
567
+ /** Viewport büyütmesinden sonra yeniden yerleşme için tanınan üst süre (B-4). */
568
+ const resizeSettleBudgetMs = 3000;
264
569
  /**
265
570
  * Navigasyon zaman aşımında toplam deneme sayısı. Soğuk Studio'da ilk `page.goto` Vite
266
571
  * derlemesini ISITIR; ikinci deneme neredeyse her zaman geçer (09-08 S-8: aynı kare üç
@@ -316,6 +621,22 @@ async function measureContentHeight(page) {
316
621
  const height = measured?.contentHeight;
317
622
  return typeof height === 'number' && Number.isFinite(height) && height > 0 ? Math.ceil(height) : undefined;
318
623
  }
624
+ /**
625
+ * Kırpma kutusunu çözer. Bulunamayan ya da çizilmemiş düğüm BOŞ KARE değil HATA döndürür:
626
+ * "yan sütunu kırptım" deyip bomboş bir PNG almak sessiz yanlıştır.
627
+ */
628
+ async function resolveClip(page, clip) {
629
+ if (!('element' in clip))
630
+ return clip;
631
+ const raw = await page.evaluate(clipRectScript(clip.element)).catch(() => null);
632
+ if (raw === null || raw === undefined) {
633
+ throw new PlatformApiError(`page_screenshot: clip.element '${clip.element}' sayfada yok — kırpılacak kutu bulunamadı. elementId'leri page_schema_get ile doğrulayın; bir komponentin İÇ kontrolünü kırpıyorsanız <elementId>__<rol> biçimini kullanın.`);
634
+ }
635
+ if (!(raw.width > 0 && raw.height > 0)) {
636
+ throw new PlatformApiError(`page_screenshot: clip.element '${clip.element}' ÇİZİLMEMİŞ (${Math.round(raw.width)}x${Math.round(raw.height)} px) — kırpma boş kare üretirdi. Düğüm gizli ya da sıfır boyutlu olabilir; layoutReport: true ile kutusuna bakın.`);
637
+ }
638
+ return { x: Math.round(raw.x), y: Math.round(raw.y), width: Math.round(raw.width), height: Math.round(raw.height) };
639
+ }
319
640
  /**
320
641
  * Playwright bağlamı ve tarayıcısı bu sınıfta PAYLAŞILIR. İki çekim aynı anda koştuğunda
321
642
  * 2026-09-06 koşusunda ikisi de `Target page, context or browser has been closed` ile düştü
@@ -488,6 +809,11 @@ export class StudioScreenshotter {
488
809
  : navigated
489
810
  ? activityAfter.completed
490
811
  : activityBefore === null ? undefined : Math.max(activityAfter.completed - activityBefore.completed, 0);
812
+ // `tab` adımının KANITI tıklama değil, sonuçta hangi sekmenin açık olduğudur: bileşen
813
+ // seçimi kendi prop'una yazmadıysa kare hâlâ eski sekmeyi gösterir ve rapor bunu söyler.
814
+ const activeTab = action.action === 'tab' && typeof action.target === 'string'
815
+ ? await page.evaluate(activeTabScript(action.target)).catch(() => undefined)
816
+ : undefined;
491
817
  // G-14 — DOM'a yazmak değerin prop'a ulaştığını kanıtlamaz. İz yalnız okuyucu yayınlanıyor ve
492
818
  // makine düğümü tanıyorsa (dize) karşılaştırılır; aksi hâlde propWritten bilinmez kalır.
493
819
  const propsAfter = typeof before.propsBefore === 'string' && before.applied === true && !navigated
@@ -509,6 +835,11 @@ export class StudioScreenshotter {
509
835
  ...(note === undefined ? {} : { note }),
510
836
  requests,
511
837
  ...(flowRuns === undefined ? {} : { flowRuns }),
838
+ ...(before.matchedBy === undefined ? {} : { matchedBy: before.matchedBy }),
839
+ ...(typeof activeTab === 'string' ? { activeTab } : {}),
840
+ ...(before.optionsSeen === undefined ? {} : { optionsSeen: before.optionsSeen }),
841
+ ...(before.waitedMs === undefined ? {} : { waitedMs: before.waitedMs }),
842
+ ...(before.submitVia === undefined ? {} : { submitVia: before.submitVia }),
512
843
  pageState: readiness.state
513
844
  });
514
845
  }
@@ -563,6 +894,18 @@ export class StudioScreenshotter {
563
894
  }
564
895
  return { state: 'not-ready', waitedMs: Date.now() - startedAt, ...(last === undefined ? {} : { snapshot: last }) };
565
896
  }
897
+ /**
898
+ * Viewport büyütmesinden sonra sayfanın YENİDEN YERLEŞMESİNİ bekler. Eski sırada (ölç → büyüt →
899
+ * eylemler) büyütme sonrası render'ı her eylemin kendi hazırlık döngüsü soğuruyordu; sıra
900
+ * eylemlerden sonraya alınınca geriye yalnız 600 ms kalmıştı ve `onResize`'a bağlı ya da
901
+ * sanallaştırılmış bir tablo o pay içinde yeni satırlarını çizmeyebilirdi — özet ve kare
902
+ * büyütme ÖNCESİ gerçeği anlatırdı (B-4).
903
+ */
904
+ async settleAfterResize(page, baselineUrl) {
905
+ await page.waitForTimeout(settleMs);
906
+ // Yeni akış beklenmiyor: karar DOM durulmasından çıkar (`ready-no-flows` / `ready-no-counter`).
907
+ await this.waitForReady(page, { budgetMs: resizeSettleBudgetMs, baselineUrl, completedBefore: Number.POSITIVE_INFINITY });
908
+ }
566
909
  async captureOne(request) {
567
910
  const startedAt = Date.now();
568
911
  if (this.idleTimer)
@@ -574,7 +917,7 @@ export class StudioScreenshotter {
574
917
  });
575
918
  let context;
576
919
  try {
577
- context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: 1 });
920
+ context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: request.scale ?? 1 });
578
921
  }
579
922
  catch {
580
923
  this.browserPromise = this.launch();
@@ -582,7 +925,7 @@ export class StudioScreenshotter {
582
925
  this.browserPromise = undefined;
583
926
  throw error;
584
927
  });
585
- context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: 1 });
928
+ context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: request.scale ?? 1 });
586
929
  }
587
930
  logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · gateway jetonu YOK'}${(request.cookies ?? []).length > 0 ? ` · caller çerezleri: ${(request.cookies ?? []).map(cookie => cookie.name).join(', ')}` : ''}${request.token || (request.cookies ?? []).length > 0 ? '' : ' · ANONİM (çerez YOK)'}${request.colorScheme ? ` · tema ${request.colorScheme}` : ''}`);
588
931
  try {
@@ -653,27 +996,75 @@ export class StudioScreenshotter {
653
996
  : readiness.state === 'not-ready' ? false : undefined;
654
997
  const finalHref = readiness.snapshot?.href;
655
998
  const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
999
+ const actions = await this.runActions(page, request);
1000
+ // Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
1001
+ // boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
1002
+ // tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
1003
+ // (S-1, 2026-09-16 raporu madde 6). Yalnız YÜKSEKLİK büyür, genişlik sabittir: `hideOn`
1004
+ // kırılım noktaları ve duyarlı yerleşim kareyi çekerken kaymaz (K4).
656
1005
  const measured = request.fullPage ? await measureContentHeight(page) : undefined;
657
1006
  const captureHeight = measured === undefined ? undefined : Math.min(measured, maxCaptureHeight);
658
- if (captureHeight !== undefined && captureHeight > request.viewport.height) {
1007
+ let viewportHeight = request.viewport.height;
1008
+ if (captureHeight !== undefined && captureHeight > viewportHeight) {
659
1009
  await page.setViewportSize({ width: request.viewport.width, height: captureHeight });
660
- await page.waitForTimeout(settleMs);
1010
+ viewportHeight = captureHeight;
1011
+ await this.settleAfterResize(page, readiness.snapshot?.href ?? request.url);
1012
+ }
1013
+ // K7 — kırpma kutusu katlanmış alanın ALTINDAYSA kare boş çıkardı: viewport kutuyu
1014
+ // kapsayana kadar büyütülür (fullPage ile aynı yol) ve kutu YENİDEN okunur, çünkü
1015
+ // büyüyen viewport yerleşimi kaydırabilir.
1016
+ let clip;
1017
+ if (request.clip) {
1018
+ clip = await resolveClip(page, request.clip);
1019
+ if (clip.y + clip.height > viewportHeight) {
1020
+ const wanted = Math.ceil(clip.y + clip.height) + 8;
1021
+ if (wanted > maxCaptureHeight) {
1022
+ throw new PlatformApiError(`page_screenshot: kırpma kutusu ${Math.round(clip.y)}+${Math.round(clip.height)} px'te bitiyor ve Chromium'un ${maxCaptureHeight} px'lik doku sınırının altına sığmıyor — viewport o kadar büyütülemez ve kare kutunun tamamını gösteremezdi. Kutuyu elle daraltın ({ x, y, width, height }) ya da sayfayı bölerek doğrulayın.`);
1023
+ }
1024
+ const grown = Math.min(wanted, maxCaptureHeight);
1025
+ await page.setViewportSize({ width: request.viewport.width, height: grown });
1026
+ viewportHeight = grown;
1027
+ await this.settleAfterResize(page, readiness.snapshot?.href ?? request.url);
1028
+ clip = await resolveClip(page, request.clip);
1029
+ }
661
1030
  }
662
- const actions = await this.runActions(page, request);
663
1031
  const layout = request.layoutReport === true
664
1032
  ? await page.evaluate(layoutReportScript).then(value => Array.isArray(value) ? value : []).catch(() => [])
665
1033
  : undefined;
666
1034
  const runtimeErrors = await page.evaluate(flowRuntimeErrorsScript)
667
1035
  .then(value => Array.isArray(value) ? value : [])
668
1036
  .catch(() => []);
669
- const png = await page.screenshot({ type: 'png', fullPage: false });
1037
+ // Özet kareden ÖNCE ve büyütmeden SONRA: ikisi de aynı yerleşmiş sayfayı anlatsın.
1038
+ const summaryRaw = request.summary === false
1039
+ ? undefined
1040
+ : await page.evaluate(domSummaryScript(request.viewport.height)).then(value => ({ value })).catch(error => ({ error: error instanceof Error ? error.message : String(error) }));
1041
+ const summary = summaryRaw && 'value' in summaryRaw ? parseDomSummary(summaryRaw.value) : undefined;
1042
+ const summaryUnavailable = summaryRaw === undefined
1043
+ ? undefined
1044
+ : 'error' in summaryRaw
1045
+ ? summaryRaw.error
1046
+ : summary === undefined ? 'tarayıcı özet döndürmedi (beklenmeyen biçim)' : undefined;
1047
+ // Olgu doğrulamasında kare HİÇ çekilmez: maliyetin tamamı buradadır (S-5).
1048
+ const format = request.format ?? 'png';
1049
+ const png = request.image === 'none'
1050
+ ? undefined
1051
+ : await page.screenshot({
1052
+ type: format,
1053
+ ...(format === 'jpeg' && request.quality !== undefined ? { quality: request.quality } : {}),
1054
+ fullPage: false,
1055
+ ...(clip === undefined ? {} : { clip })
1056
+ });
670
1057
  const elapsedMs = Date.now() - startedAt;
671
- logStep('tarayıcı ←', `PNG ${png.byteLength} B · ${elapsedMs} ms · hazırlık ${readiness.state} (${readiness.waitedMs} ms) · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}${finalUrl === undefined ? '' : ` · yönlendi → ${finalUrl}`}`);
1058
+ logStep('tarayıcı ←', `${png === undefined ? 'kare YOK (image:none)' : `PNG ${png.byteLength} B`} · ${elapsedMs} ms · hazırlık ${readiness.state} (${readiness.waitedMs} ms) · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}${finalUrl === undefined ? '' : ` · yönlendi → ${finalUrl}`}`);
672
1059
  if (runtimeErrors.length > 0) {
673
1060
  logStep('akış hatası ✕', `${runtimeErrors.length} akış adımı hata bıraktı — sayfa yine 200 döndü`, runtimeErrors.map(error => `${error.step?.name ?? '?'}${error.element ? ` → ${error.element}.${error.property ?? '?'}` : ''}: ${error.message ?? ''}`));
674
1061
  }
675
1062
  return {
676
- png,
1063
+ ...(png === undefined ? {} : { png, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' }),
1064
+ ...(png === undefined ? {} : (size => size === undefined ? {} : { imageSize: size })(imageSize(png))),
1065
+ ...(clip === undefined ? {} : { clip }),
1066
+ ...(summary === undefined ? {} : { summary }),
1067
+ ...(summaryUnavailable === undefined ? {} : { summaryUnavailable }),
677
1068
  apiStatus,
678
1069
  renderWaitTimedOut: apiStatus === undefined,
679
1070
  elapsedMs,
@@ -0,0 +1,55 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** Alan sırasından bağımsız anahtar: aynı istek her çağrıda aynı sha256'yı üretmeli. */
3
+ function stable(value) {
4
+ if (Array.isArray(value))
5
+ return value.map(stable);
6
+ if (value && typeof value === 'object') {
7
+ const source = value;
8
+ return Object.keys(source).sort().reduce((target, key) => {
9
+ if (source[key] !== undefined)
10
+ target[key] = stable(source[key]);
11
+ return target;
12
+ }, {});
13
+ }
14
+ return value;
15
+ }
16
+ export function captureKey(parts) {
17
+ return createHash('sha256').update(JSON.stringify(stable(parts))).digest('hex');
18
+ }
19
+ export function imageHash(image) {
20
+ return createHash('sha256').update(image).digest('hex');
21
+ }
22
+ export class CaptureMemory {
23
+ limit;
24
+ entries = new Map();
25
+ constructor(limit = 50) {
26
+ this.limit = limit;
27
+ }
28
+ recall(key) {
29
+ return this.entries.get(key);
30
+ }
31
+ remember(key, entry) {
32
+ // Yeniden yazılan anahtar halkanın SONUNA gider: sık çekilen sayfa düşmesin.
33
+ this.entries.delete(key);
34
+ this.entries.set(key, entry);
35
+ while (this.entries.size > this.limit) {
36
+ const oldest = this.entries.keys().next().value;
37
+ if (oldest === undefined)
38
+ break;
39
+ this.entries.delete(oldest);
40
+ }
41
+ }
42
+ get size() {
43
+ return this.entries.size;
44
+ }
45
+ }
46
+ /**
47
+ * SÜREÇ düzeyinde tek bellek.
48
+ *
49
+ * HTTP taşıması her POST için YENİ bir gateway sunucusu kuruyor (`index.ts` `app.post('/mcp')`
50
+ * → `createGatewayServer`), yani `registerPageTools` istek başına koşuyor. Bellek o fonksiyonun
51
+ * içinde yaratıldığında her çağrı boş bir halkayla başlıyor ve `diff` her seferinde "ilk kare"
52
+ * diyordu — ölçüldü 2026-09-16, dört ardışık çağrının dördü de `baselineAt` yok. Stdio
53
+ * taşımasında sunucu oturum boyu yaşadığı için kusur orada görünmüyordu.
54
+ */
55
+ export const sharedCaptureMemory = new CaptureMemory();
@@ -6,7 +6,7 @@
6
6
  "sourceRef": "platform_guide('layout') Kural 5a/5b; zeyrek-frontend apps/studio/src/features/layout/layout.functions.ts (getDefaultLayout), packages/ng-apps-screen-builder/src/flex-layout.ts (getFlexChildStyle), packages/ng-apps-ui/src/shared/render/render.functions.ts"
7
7
  },
8
8
  "_rules": {
9
- "viewsNotSchema": "Layout'ta şema alanının adı 'views'tir; micro-op hattı yoktur, değişiklik layout_update ile tam gövde olarak yazılır (LAY-001).",
9
+ "viewsNotSchema": "Layout'ta şema alanının adı 'views'tir (LAY-001). Kurulu layout'ta hedefli değişiklik layout_schema_patch micro-op'larıyla yazılır; tam ağaç layout_update ile.",
10
10
  "singleSlot": "TEK slot. Sayfa şeması HER slot'a ayrı ayrı kopyalanır — iki slot sayfayı iki kez çizer ve elementId'leri tekrarlar (LAY-002 / LAY-006).",
11
11
  "propValueField": "Statik prop'un değeri 'data' alanındadır ({ type:'static', ref:null, data }); 'ref' referans tipleri (flow/file) içindir. Ham değer yazarsanız gateway sarmalar ve normalizasyon notu döner.",
12
12
  "slotRowIsOpenEnded": "Slot'un grid satırı açık uçludur (runtime 'y / auto' yazar): sayfa uzadıkça slot uzar. Slot'a sabit yükseklik vermeye çalışma.",