@appaflytech/zeyrek-ai-mcp 0.0.18 → 0.0.20

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 (65) hide show
  1. package/README.md +13 -14
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/componentCatalog.js +204 -836
  4. package/dist/knowledge/componentEvents.js +75 -368
  5. package/dist/knowledge/mobileComponentCatalog.js +331 -0
  6. package/dist/knowledge/mobileComponentEvents.js +50 -0
  7. package/dist/knowledge/normalize.js +63 -66
  8. package/dist/knowledge/platform/lint.js +205 -0
  9. package/dist/knowledge/platform/mobile.js +78 -0
  10. package/dist/knowledge/platform/mobileRules.js +121 -0
  11. package/dist/knowledge/platform/mobileTabs.js +108 -0
  12. package/dist/knowledge/platform/profile.js +20 -0
  13. package/dist/knowledge/platform/resolve.js +122 -0
  14. package/dist/knowledge/platform/web.js +38 -0
  15. package/dist/knowledge/rules.js +6 -29
  16. package/dist/knowledge/styleResolve.js +1 -1
  17. package/dist/knowledge/styleResolveNative.js +410 -0
  18. package/dist/knowledge/themeResolve.js +3 -3
  19. package/dist/platform/capture/actions.js +333 -0
  20. package/dist/platform/capture/browser.js +1 -0
  21. package/dist/platform/{screenshot.js → capture/capture.js} +23 -509
  22. package/dist/platform/capture/index.js +10 -0
  23. package/dist/platform/capture/readiness.js +103 -0
  24. package/dist/platform/capture/summary.js +77 -0
  25. package/dist/platform/capture/target.js +5 -0
  26. package/dist/platform/capture/targets/studio-mobile.js +15 -0
  27. package/dist/platform/capture/targets/studio-web.js +5 -0
  28. package/dist/platform/domSummary.js +1 -1
  29. package/dist/platform/responses.js +1 -2
  30. package/dist/platform/strictArguments.js +2 -22
  31. package/dist/platform/writeDiff.js +31 -58
  32. package/dist/resources/examples/mobile-app-shell-drawer.json +209 -0
  33. package/dist/resources/examples/mobile-app-shell-stack.json +123 -0
  34. package/dist/resources/examples/mobile-app-shell-tab.json +174 -0
  35. package/dist/resources/examples/mobile-list-detail-form.json +656 -0
  36. package/dist/resources/guides/component.md +6 -9
  37. package/dist/resources/guides/functions.md +3 -32
  38. package/dist/resources/guides/getting-started.md +0 -35
  39. package/dist/resources/guides/layout.md +11 -7
  40. package/dist/resources/guides/mobile.md +215 -0
  41. package/dist/resources/guides/page.md +71 -49
  42. package/dist/resources/guides/translation.md +6 -5
  43. package/dist/resources/schema-fingerprint.json +2 -2
  44. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +0 -18
  45. package/dist/server.js +2 -0
  46. package/dist/tools/applications.js +60 -3
  47. package/dist/tools/components.js +50 -10
  48. package/dist/tools/designSchemaPatch.js +18 -13
  49. package/dist/tools/externalComponents.js +25 -4
  50. package/dist/tools/flows.js +51 -38
  51. package/dist/tools/functions.js +10 -20
  52. package/dist/tools/layouts.js +41 -9
  53. package/dist/tools/microservices.js +11 -39
  54. package/dist/tools/mobile.js +203 -0
  55. package/dist/tools/pages.js +127 -95
  56. package/dist/tools/platformInfo.js +2 -1
  57. package/dist/tools/profileWrite.js +45 -0
  58. package/dist/tools/schemaOps.js +2 -2
  59. package/dist/tools/screenWrite.js +2 -2
  60. package/dist/tools/screens.js +9 -3
  61. package/dist/tools/workflows.js +12 -39
  62. package/package.json +2 -7
  63. package/dist/platform/diagnostics.js +0 -85
  64. package/dist/platform/ifExists.js +0 -129
  65. package/dist/platform/writeView.js +0 -17
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Kare ne zaman hazır? (G-30 → B-6, 2026-09-07)
3
+ *
4
+ * `delayMs` TAHMİNDİ; 09-05'te runtime koşan akış sayacını yayınlamaya başladı (`@ng-apps/flow`
5
+ * → flow-runtime-errors.ts) ve bekleme "running sıfır, completed > 0" olunca bitiyordu. Bu şart
6
+ * mount akışı OLMAYAN sayfada (giriş ekranı) hiç sağlanmaz; akış tetiklemeyen `set` eylemi de
7
+ * `completed`'ı artırmaz. 09-06·2 koşusunda üç eylemli giriş denemesi bu yüzden 47 sn sürdü;
8
+ * sentetik ölçümde sayfa 0.3 sn'de hazırken aynı çağrı 48 sn'ydi — süre sayfanın değil, bu
9
+ * döngünün. Hazırlık artık TEK anlık görüntüden okunur: akış sayacı + Studio'nun yükleme
10
+ * iskeleti (`ViewDetail.svelte` → `.view-detail > .bx--skeleton__placeholder`) + runtime
11
+ * düğümleri (`[data-element-id]`) + hata/bulunamadı hâli + adres. Karar `decideReadiness`'ta.
12
+ *
13
+ * Yüklenmemiş kare ASLA dönmez: bütçe dolduğunda sayfa hâlâ hazır değilse çağrı hatayla düşer
14
+ * (`captureUnready: true` teşhis kipi dışında). Bozuk sayfayı görmek istiyorsan hata mesajı
15
+ * neyin görüldüğünü zaten söyler.
16
+ */
17
+ export const readinessScript = `(function () { /* readinessSnapshot */
18
+ var activity = window.__ngAppsFlowActivity;
19
+ return {
20
+ activity: activity && typeof activity.running === 'number' && typeof activity.completed === 'number' ? { running: activity.running, completed: activity.completed } : null,
21
+ runtimeNodes: document.querySelectorAll('[data-element-id]').length,
22
+ skeleton: !!document.querySelector('.view-detail > .bx--skeleton__placeholder'),
23
+ viewDetail: !!document.querySelector('.view-detail'),
24
+ errorState: !!document.querySelector('.view-detail__error, .view-detail .error-page'),
25
+ readyState: String(document.readyState),
26
+ href: String(location.href)
27
+ };
28
+ })()`;
29
+ /** Okunan değer gerçekten sayaç mı; değilse sayfa sayaç yayınlamıyordur (eski runtime). */
30
+ export function isFlowActivity(activity) {
31
+ if (!activity || typeof activity !== 'object')
32
+ return false;
33
+ const { running, completed } = activity;
34
+ return typeof running === 'number' && typeof completed === 'number';
35
+ }
36
+ /** Tarayıcıdan dönen değer anlık görüntü mü; yönlendirme sırasında evaluate düşer ve undefined kalır. */
37
+ export function parseReadiness(raw) {
38
+ if (!raw || typeof raw !== 'object')
39
+ return undefined;
40
+ const value = raw;
41
+ if (typeof value.runtimeNodes !== 'number' || typeof value.href !== 'string')
42
+ return undefined;
43
+ return {
44
+ activity: isFlowActivity(value.activity) ? { running: value.activity.running, completed: value.activity.completed } : null,
45
+ runtimeNodes: value.runtimeNodes,
46
+ skeleton: value.skeleton === true,
47
+ viewDetail: value.viewDetail === true,
48
+ errorState: value.errorState === true,
49
+ readyState: String(value.readyState ?? ''),
50
+ href: value.href
51
+ };
52
+ }
53
+ /** `/dev` ve `/view` aynı sayfadır (Studio `hooks.ts` reroute); yönlendirme kararı yalnız yoldan verilir. */
54
+ export function runtimePath(url) {
55
+ try {
56
+ return new URL(url).pathname.replace(/^\/dev\//, '/view/').replace(/\/+$/, '');
57
+ }
58
+ catch {
59
+ return url;
60
+ }
61
+ }
62
+ /** "Hiç akış koşmadı" kararı için DOM'un kaç yoklama değişmemesi gerekir (~600 ms = eski settleMs). */
63
+ const quietPolls = 5;
64
+ /** Sayaç yayınlanmayan runtime'da tek kanıt DOM durulmasıdır; daha uzun beklenir (~1 sn). */
65
+ const legacyQuietPolls = 8;
66
+ /** Runtime dışına yönlendirme: hedef sayfanın kendi JS'i çizmeyi bitirsin (~1.5 sn). */
67
+ const redirectQuietPolls = 12;
68
+ /**
69
+ * Tek anlık görüntüden "kare alınabilir" kararı; iki uçta da yanlış beklememek için tek yerde.
70
+ * `quiet` görüntünün kaç yoklamadır değişmediğidir, `completedBefore` bu beklemenin başındaki
71
+ * bitmiş akış sayısı (sayfa değiştiyse çağıran sıfırlar). İskelet görünüyorsa hiçbir karar yok.
72
+ */
73
+ export function decideReadiness(snapshot, quiet, completedBefore, baselineUrl) {
74
+ if (snapshot.skeleton)
75
+ return undefined;
76
+ const { activity } = snapshot;
77
+ const idle = activity !== null && activity.running === 0;
78
+ if (idle && activity.completed > completedBefore && snapshot.runtimeNodes > 0)
79
+ return 'ready';
80
+ if (idle && snapshot.runtimeNodes > 0 && quiet >= quietPolls)
81
+ return 'ready-no-flows';
82
+ if (activity === null && snapshot.runtimeNodes > 0 && quiet >= legacyQuietPolls)
83
+ return 'ready-no-counter';
84
+ if (idle && snapshot.errorState && quiet >= quietPolls)
85
+ return 'error';
86
+ if (!snapshot.viewDetail && snapshot.readyState === 'complete' && baselineUrl !== undefined && runtimePath(snapshot.href) !== runtimePath(baselineUrl) && quiet >= redirectQuietPolls)
87
+ return 'redirected';
88
+ return undefined;
89
+ }
90
+ /** Hata mesajı için: bütçe dolduğunda sayfada NE görüldü. */
91
+ export function describeUnready(readiness, apiNote) {
92
+ const snapshot = readiness.snapshot;
93
+ const suffix = apiNote === undefined ? '' : ` · ${apiNote}`;
94
+ if (!snapshot)
95
+ return `tarayıcı sayfayı hiç değerlendiremedi (sürekli yönlendirme ya da bağlam düştü)${suffix}`;
96
+ if (snapshot.skeleton)
97
+ return `sayfa hâlâ yükleme iskeletinde${suffix}`;
98
+ if (snapshot.activity && snapshot.activity.running > 0)
99
+ return `${snapshot.activity.running} akış hâlâ koşuyor (${snapshot.activity.completed} bitti)${suffix}`;
100
+ if (snapshot.runtimeNodes === 0)
101
+ return `runtime hiç render olmadı — data-element-id taşıyan düğüm yok${snapshot.viewDetail ? '' : ', .view-detail kabı da yok'}${suffix}`;
102
+ return `sayfa durulmadı (${snapshot.runtimeNodes} düğüm, sayaç ${snapshot.activity ? `${snapshot.activity.running} koşan / ${snapshot.activity.completed} biten` : 'yok'})${suffix}`;
103
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * NE GÖRÜNÜYOR — resimsiz (S-5). Aynı sayfa yüklemesinden çıkan üçüncü çıktı: her KÖK runtime
3
+ * düğümünün görünürlüğü, metni, satır sayısı, açık sekmesi, değeri ve hata metinleri.
4
+ *
5
+ * Kök düğüm = kimliğinde `__` OLMAYAN düğüm; iç roller (`tbl__row-3`) satır sayısına dönüşür,
6
+ * listeye girmez. Tavan 300 düğüm, metin 80 karakter (K3) — özet bir kareyi değiştirmeli, onun
7
+ * kadar pahalı olmamalı. Tip `data-component-type`'tan gelir (frontend f-02); runtime basmıyorsa
8
+ * alan YAZILMAZ ve `typed: false` döner — `layoutReportUnavailable` ile aynı dürüstlük kuralı.
9
+ */
10
+ export function domSummaryScript(viewportHeight) {
11
+ return `(function () { /* domSummary */
12
+ var MAX = 300;
13
+ var CUT = 80;
14
+ // Görünür alanın GERÇEK boyu: fullPage ve clip yolları viewport'u içerik boyuna büyütür ve
15
+ // o anda window.innerHeight = içerik yüksekliği olur — her düğüm "ekran içinde" görünürdü ve
16
+ // "kart katlanmanın altında mı" sorusu HER ZAMAN 'visible' cevabı alırdı (B-10).
17
+ var VH = ${Math.max(Math.round(viewportHeight), 1)};
18
+ var clean = function (value) { return String(value || '').replace(/\\s+/g, ' ').trim(); };
19
+ // Mobil sekme çubuğu '__tabs' bir KÖK satırdır (öğeleri '__tabs__item-<slug>' iç roldür, K14);
20
+ // web runtime bu kimliği hiç basmaz.
21
+ var ROOT_SELECTOR = '[data-element-id]:not([data-element-id*="__"]), [data-element-id="__tabs"]';
22
+ var isRoot = function (element) { var id = String(element.getAttribute('data-element-id') || ''); return id.indexOf('__') < 0 || id === '__tabs'; };
23
+ var all = Array.prototype.slice.call(document.querySelectorAll('[data-element-id]'));
24
+ var roots = all.filter(isRoot);
25
+ var typed = false;
26
+ var nodes = roots.slice(0, MAX).map(function (node) {
27
+ var id = node.getAttribute('data-element-id');
28
+ var type = node.getAttribute('data-component-type');
29
+ if (type) typed = true;
30
+ // Bir olgu, ONU TAŞIYAN düğüme yazılır: en yakın kök ata bu düğüm değilse o olgu
31
+ // çocuğundur ve orada zaten listelenecektir (B-8). Aksi hâlde aynı değer üç satırda çıkar.
32
+ var owned = function (element) { return !!element && element.closest(ROOT_SELECTOR) === node; };
33
+ var rect = node.getBoundingClientRect();
34
+ var visible = typeof node.checkVisibility === 'function'
35
+ ? !!node.checkVisibility({ checkVisibilityCSS: true, checkOpacity: true })
36
+ : (rect.width > 0 && rect.height > 0);
37
+ var inViewport = rect.bottom > 0 && rect.right > 0 && rect.top < VH && rect.left < (window.innerWidth || 0);
38
+ // Kap düğümün innerText'i ÇOCUKLARININ metnidir; metin yalnız YAPRAK köklerde yazılır.
39
+ var nested = Array.prototype.slice.call(node.querySelectorAll('[data-element-id]'))
40
+ .some(isRoot);
41
+ var text = nested ? '' : clean(node.innerText);
42
+ if (text.length > CUT) text = text.slice(0, CUT) + '…';
43
+ var rows = document.querySelectorAll('[data-element-id^="' + id + '__row-"]').length;
44
+ var tab = node.querySelector('[role="tab"][aria-selected="true"]');
45
+ var field = node.querySelector('input, select, textarea');
46
+ var chosen = node.querySelector('.selected-item');
47
+ var holder = field || chosen;
48
+ var value = !owned(holder) ? '' : (field ? (field.type === 'password' ? '***' : clean(field.value)) : clean(chosen.textContent));
49
+ var media = node.querySelector('img, svg, canvas, video, iframe');
50
+ // İç içe <li> hem grubu hem alt mesajları sayıyordu: tek grup + 2 alt hata 3 görünüyordu (B-7).
51
+ var errorItems = Array.prototype.slice.call(node.querySelectorAll('.form-validation-errors li:not(:has(li))'))
52
+ .filter(owned).map(function (item) { return clean(item.textContent); });
53
+ // aria-invalid hem köke hem input'a yazılabiliyor (Password.svelte); yalnız ALANLAR sayılır.
54
+ var invalid = Array.prototype.slice.call(node.querySelectorAll('input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"]')).filter(owned).length;
55
+ var summary = { elementId: id, visible: !!visible, inViewport: !!inViewport };
56
+ if (type) summary.type = type;
57
+ if (text) summary.text = text;
58
+ if (rows > 0) summary.rows = rows;
59
+ if (owned(tab)) summary.activeTab = clean(tab.textContent);
60
+ if (value) summary.value = value;
61
+ if (visible && !text && !media && !nested) summary.empty = true;
62
+ if (errorItems.length > 0) summary.errors = errorItems;
63
+ else if (invalid > 0) summary.errors = ['aria-invalid: ' + invalid];
64
+ return summary;
65
+ });
66
+ var messages = [];
67
+ Array.prototype.slice.call(document.querySelectorAll('.ng-toast')).forEach(function (toast) {
68
+ var name = String(toast.className || '');
69
+ var kind = name.indexOf('ng-toast--error') >= 0 ? 'toast/error' : (name.indexOf('ng-toast--success') >= 0 ? 'toast/success' : 'toast');
70
+ messages.push({ kind: kind, text: clean(toast.innerText).slice(0, 160) });
71
+ });
72
+ Array.prototype.slice.call(document.querySelectorAll('.view-detail__error, .error-page')).forEach(function (node) {
73
+ messages.push({ kind: 'error', text: clean(node.innerText).slice(0, 160) });
74
+ });
75
+ return { nodes: nodes, messages: messages, truncated: roots.length > MAX, typed: typed };
76
+ })()`;
77
+ }
@@ -0,0 +1,5 @@
1
+ import { studioMobileTarget } from './targets/studio-mobile.js';
2
+ import { studioWebTarget } from './targets/studio-web.js';
3
+ export function captureTargetFor(name) {
4
+ return name === 'studio-mobile' ? studioMobileTarget : studioWebTarget;
5
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Mobil uygulama: `@ng-apps/ui-native` `NativeRuntime`, Studio'da react-native-web ile çizilir
3
+ * (plans/mobil-uygulama-expo F5). Ağaçta `<form>` yoktur — `submit` runtime'ın yayımladığı köprüyle
4
+ * form düğümünün `submit` olayını makineye iletir (`web-runtime-host.tsx` `__ngAppsSubmitForm`).
5
+ * Sekme çubuğu `__tabs`, öğeleri `__tabs__item-<sayfa slug'ı>` kimliğini taşır (K14). Açılır
6
+ * listenin tetikleyicisi `role="combobox"`, seçenekleri `role="option"` + `data-value` taşır ve liste
7
+ * `Modal` katmanında açılır (`ng-apps-ui-native` `dropdown`).
8
+ */
9
+ export const studioMobileTarget = Object.freeze({
10
+ name: 'studio-mobile',
11
+ submitBridge: '__ngAppsSubmitForm',
12
+ optionSelector: '[role="option"][data-value], [role="treeitem"][data-value]',
13
+ optionsOutsideNode: true,
14
+ platformNote: 'Mobil uygulama: kare Studio\'daki react-native-web çizimidir (/dev|/view) — cihazdaki native çizimle yapısal olarak (düğüm, sıra, ölçü modu, renk) aynı, piksel olarak değil.'
15
+ });
@@ -0,0 +1,5 @@
1
+ /** Web uygulaması: Svelte runtime. Seçenekler `ng-select` ağacı, form gerçek `<form>`. */
2
+ export const studioWebTarget = Object.freeze({
3
+ name: 'studio-web',
4
+ optionSelector: '[role="treeitem"][data-value]'
5
+ });
@@ -7,7 +7,7 @@
7
7
  * (card) · GİZLİ" 200 karakterdir.
8
8
  *
9
9
  * Katman saftır: tarayıcıdan gelen anlık görüntüyü doğrular ve biçimler. Toplama işi
10
- * `screenshot.ts` `domSummaryScript`'te, sunum `pages.ts`'te.
10
+ * `capture/summary.ts` `domSummaryScript`'te, sunum `pages.ts`'te.
11
11
  */
12
12
  function text(value) {
13
13
  return typeof value === 'string' && value.length > 0 ? value : undefined;
@@ -1,4 +1,3 @@
1
- import { compactDiagnostics } from './diagnostics.js';
2
1
  import { logError, logStep } from '../observability/trace.js';
3
2
  import { PlatformApiError, describeErrorBody } from './errors.js';
4
3
  import { PolicyBlockError } from '../knowledge/policy.js';
@@ -140,7 +139,7 @@ function asStructured(payload) {
140
139
  return { data: payload };
141
140
  }
142
141
  export function toolResult(payload, config, expectFailure) {
143
- const pruned = prune(compactDiagnostics(payload));
142
+ const pruned = prune(payload);
144
143
  const { payload: safe, masked, tokenRefs } = maskSecrets(pruned);
145
144
  const runtimeErrors = runtimeErrorSummary(safe);
146
145
  if (runtimeErrors.length > 0) {
@@ -1,15 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { diagnosticsParam, runWithDiagnostics } from './diagnostics.js';
3
- /**
4
- * `writes`/`updates` açıklamalı ama KAYIT yazmayan tool'lar: çalıştırma, önizleme, dışa aktarma, indirme,
5
- * bağlantı testi. Yanıtlarında yazma tanılaması (silentlyDropped) yoktur; parametre yalnız tool listesini
6
- * büyütür. Yeni böyle bir tool eklenirse buraya yazılır — `test/diagnostics.test.ts` listeyi kilitler.
7
- */
8
- const NON_WRITING_TOOLS = new Set([
9
- 'microservice_execute', 'microservice_preview', 'microservice_debug', 'query_preview', 'workflow_preview',
10
- 'automation_preview', 'automation_trigger', 'row_export', 'file_download', 'microfunction_invoke',
11
- 'microfunction_runtime_test_connection'
12
- ]);
13
2
  function isRawShape(value) {
14
3
  if (!value || typeof value !== 'object' || '_def' in value)
15
4
  return false;
@@ -30,23 +19,14 @@ export function rejectUnknownArguments(server) {
30
19
  server.registerTool = ((name, config, callback) => {
31
20
  if (!isRawShape(config.inputSchema))
32
21
  return registerTool(name, config, callback);
33
- // Yazma tool'ları tanılama kipini alır; parametre handler'a ULAŞMAZ (gövdeyi `...rest` ile
34
- // platforma geçiren tool'larda sızardı), yalnız toolResult'ın okuduğu bağlama konur.
35
- const writesDiagnostics = config.annotations?.readOnlyHint === false && config.annotations.destructiveHint !== true && !NON_WRITING_TOOLS.has(name);
36
- const shape = writesDiagnostics ? { ...config.inputSchema, diagnostics: diagnosticsParam } : config.inputSchema;
37
- const run = writesDiagnostics
38
- ? ((args, extra) => {
39
- const { diagnostics, ...rest } = args ?? {};
40
- return runWithDiagnostics(diagnostics === 'full' ? 'full' : 'compact', () => callback(rest, extra));
41
- })
42
- : callback;
22
+ const shape = config.inputSchema;
43
23
  const accepted = Object.keys(shape).join(', ');
44
24
  const inputSchema = z.object(shape, {
45
25
  errorMap: (issue, context) => issue.code === 'unrecognized_keys'
46
26
  ? { message: `${name} şu üst düzey argümanları tanımıyor: ${issue.keys.map(key => `'${key}'`).join(', ')}. Tanınmayan alan sessizce yok sayılmaz — çağrı hiç gönderilmedi. Bu tool'un okuduğu alanlar: ${accepted}.` }
47
27
  : { message: context.defaultError }
48
28
  }).strict();
49
- return registerTool(name, { ...config, inputSchema }, run);
29
+ return registerTool(name, { ...config, inputSchema }, callback);
50
30
  });
51
31
  return server;
52
32
  }
@@ -1,17 +1,15 @@
1
1
  import { PROP_VALUE_TYPES } from '../knowledge/normalize.js';
2
2
  import { logStep } from '../observability/trace.js';
3
3
  const maxDrops = 20;
4
- const maxCollected = 1000;
5
4
  const maxDepth = 12;
6
5
  const missing = Symbol('missing');
7
6
  /**
8
7
  * Platformun kendi doldurduğu alanlar. Yalnız gönderilen tarafta da varsalar (patch yolunda gövde
9
8
  * okunup geri yazılır) fark üretirlerdi ve bu fark hep gürültüdür: `audit.updatedAt` her yazmada
10
- * değişir. 09-05 koşusunda `microservice_patch` yanıtındaki iki satırdan biri buydu. `version` iyimser
11
- * eşzamanlılık sayacıdır: okunan belge geri yazılınca platform onu artırır, fark her zaman gürültüdür.
9
+ * değişir. 09-05 koşusunda `microservice_patch` yanıtındaki iki satırdan biri buydu.
12
10
  */
13
11
  const serverManagedKeys = new Set([
14
- 'audit', 'createdAt', 'updatedAt', 'createdUserId', 'createdUserName', 'updatedUserId', 'updatedUserName', 'version'
12
+ 'audit', 'createdAt', 'updatedAt', 'createdUserId', 'createdUserName', 'updatedUserId', 'updatedUserName'
15
13
  ]);
16
14
  /**
17
15
  * Platformun KENDİ ürettiği kimlik. Gönderilen id kaydedilenden farklıysa bu bir kayıp değildir:
@@ -52,29 +50,6 @@ export const unstoredKeys = new Set(['fieldSlug']);
52
50
  const canonicalValues = {
53
51
  type: [['string', 'text']]
54
52
  };
55
- export function emptyWriteDiff() {
56
- return { dropped: [], normalized: [], droppedCount: 0, droppedCountCapped: false, notes: [] };
57
- }
58
- function addDrop(sink, line) {
59
- if (sink.droppedCount >= maxCollected) {
60
- sink.droppedCountCapped = true;
61
- return;
62
- }
63
- sink.dropped.push(line);
64
- sink.droppedCount++;
65
- }
66
- /** Listeyi `maxDrops` satırda keser; sayı ve tavan bilgisi notlara yazılır. */
67
- function finishReport(sink) {
68
- if (sink.dropped.length > maxDrops) {
69
- const total = sink.droppedCountCapped ? `en az ${sink.droppedCount}` : `toplam ${sink.droppedCount}`;
70
- sink.dropped = sink.dropped.slice(0, maxDrops);
71
- sink.notes.push(`ilk ${maxDrops} fark listelendi, ${total} — gövdeyi *_get ile okuyup karşılaştırın.`);
72
- }
73
- else if (sink.droppedCountCapped) {
74
- sink.notes.push(`sayım en az ${sink.droppedCount} farkta durdu.`);
75
- }
76
- return sink;
77
- }
78
53
  /**
79
54
  * Yolun son alan adı; `tags[0]` ya da `fields[fieldId=x]` gibi indeks/anahtar ekini atar.
80
55
  */
@@ -106,11 +81,11 @@ function lookup(saved, key) {
106
81
  return Object.hasOwn(record, key) ? record[key] : missing;
107
82
  }
108
83
  function compare(sent, saved, path, sink, depth) {
109
- if (sink.droppedCountCapped || depth > maxDepth)
84
+ if (sink.dropped.length >= maxDrops || depth > maxDepth)
110
85
  return;
111
86
  if (Array.isArray(sent)) {
112
87
  if (saved === missing) {
113
- addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
88
+ sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
114
89
  return;
115
90
  }
116
91
  const key = Array.isArray(saved) ? identityKeyFor(sent) : undefined;
@@ -128,18 +103,18 @@ function compare(sent, saved, path, sink, depth) {
128
103
  }
129
104
  return;
130
105
  }
131
- const before = sink.droppedCount;
106
+ const before = sink.dropped.length;
132
107
  sent.forEach((entry, index) => compare(entry, lookup(saved, index), `${path}[${index}]`, sink, depth + 1));
133
108
  // Sıra tabanlı karşılaştırmada "değişmiş" görünen öğe aslında yer değiştirmiş olabilir. Tek
134
109
  // elemanlı dizide sıra farkı imkânsız olduğu için not düşülmez (2026-09-06 · S-1).
135
- if (Array.isArray(saved) && sent.length > 1 && saved.length > 1 && sink.droppedCount > before) {
136
- sink.notes.push(`${path}: (sıra tabanlı karşılaştırma — anahtar yok) aşağıdaki farklar platform diziyi başka sırada sakladıysa yanıltıcıdır`);
110
+ if (Array.isArray(saved) && sent.length > 1 && saved.length > 1 && sink.dropped.length > before) {
111
+ sink.dropped.splice(before, 0, `${path}: (sıra tabanlı karşılaştırma — anahtar yok) aşağıdaki farklar platform diziyi başka sırada sakladıysa yanıltıcıdır`);
137
112
  }
138
113
  return;
139
114
  }
140
115
  if (sent && typeof sent === 'object') {
141
116
  if (saved === missing) {
142
- addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
117
+ sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
143
118
  return;
144
119
  }
145
120
  for (const [key, entry] of Object.entries(sent)) {
@@ -155,7 +130,7 @@ function compare(sent, saved, path, sink, depth) {
155
130
  if (unstoredKeys.has(field) && (saved === missing || saved === null))
156
131
  return;
157
132
  if (saved === missing) {
158
- addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
133
+ sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
159
134
  return;
160
135
  }
161
136
  if (sent === saved)
@@ -166,7 +141,7 @@ function compare(sent, saved, path, sink, depth) {
166
141
  sink.normalized.push(`${path}: gönderilen ${render(sent)} platformun kanonik adıyla ${render(saved)} olarak saklandı — eşdeğer, kayıp değil`);
167
142
  return;
168
143
  }
169
- addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen ${render(saved)}`);
144
+ sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen ${render(saved)}`);
170
145
  }
171
146
  /**
172
147
  * Sol taraf platforma GİDEN gövdedir (gateway normalizasyonu uygulandıktan sonrası), sağ taraf
@@ -177,8 +152,8 @@ function compare(sent, saved, path, sink, depth) {
177
152
  * İki liste döner: `dropped` gerçekten kaybolan alanlar, `normalized` platformun kanonik adıyla
178
153
  * sakladığı eşdeğerler. İkincisi `gatewayNormalized`a gider, uyarıya değil.
179
154
  */
180
- export function writeDiff(sent, saved, aliases = {}, finish = true) {
181
- const sink = emptyWriteDiff();
155
+ export function writeDiff(sent, saved, aliases = {}) {
156
+ const sink = { dropped: [], normalized: [] };
182
157
  if (!sent || typeof sent !== 'object')
183
158
  return sink;
184
159
  const wrapper = saved && typeof saved === 'object' && !Array.isArray(saved)
@@ -196,16 +171,19 @@ export function writeDiff(sent, saved, aliases = {}, finish = true) {
196
171
  if (keys.length > 0 && shared.length === 0)
197
172
  return sink;
198
173
  compare(comparable, root, '', sink, 0);
199
- return finish ? finishReport(sink) : sink;
174
+ if (sink.dropped.length >= maxDrops) {
175
+ sink.dropped.push(`… ilk ${maxDrops} fark listelendi, daha fazla fark var — gövdeyi *_get ile okuyup karşılaştırın.`);
176
+ }
177
+ return sink;
200
178
  }
201
179
  export function silentlyDropped(sent, saved, aliases = {}) {
202
180
  return writeDiff(sent, saved, aliases).dropped;
203
181
  }
204
182
  export function withWriteDiff(payload, diff) {
205
- const drops = diff.dropped;
206
- const normalized = diff.normalized ?? [];
183
+ const drops = Array.isArray(diff) ? diff : diff.dropped;
184
+ const normalized = Array.isArray(diff) ? [] : diff.normalized;
207
185
  if (drops.length > 0) {
208
- logStep('sessiz düşme ⚠', `${diff.droppedCount}${diff.droppedCountCapped ? '+' : ''} alan gönderildi ama kayda geçmedi — platform hata vermeden yok saydı, yanıta silentlyDropped olarak eklenir`, [...drops, ...diff.notes]);
186
+ logStep('sessiz düşme ⚠', `${drops.length} alan gönderildi ama kayda geçmedi — platform hata vermeden yok saydı, yanıta silentlyDropped olarak eklenir`, drops);
209
187
  }
210
188
  if (normalized.length > 0) {
211
189
  logStep('düzeltme', `platform ${normalized.length} alanı kanonik adıyla sakladı — eşdeğer, yanıta gatewayNormalized olarak eklenir`, normalized);
@@ -217,9 +195,7 @@ export function withWriteDiff(payload, diff) {
217
195
  const existing = payload.gatewayNormalized;
218
196
  return {
219
197
  ...payload,
220
- ...(drops.length > 0 ? { silentlyDropped: drops, droppedCount: diff.droppedCount } : {}),
221
- ...(drops.length > 0 && diff.droppedCountCapped ? { droppedCountCapped: true } : {}),
222
- ...(drops.length > 0 && diff.notes.length > 0 ? { silentlyDroppedNotes: diff.notes } : {}),
198
+ ...(drops.length > 0 ? { silentlyDropped: drops } : {}),
223
199
  ...(normalized.length > 0
224
200
  ? { gatewayNormalized: [...(Array.isArray(existing) ? existing : []), ...normalized] }
225
201
  : {})
@@ -271,9 +247,9 @@ function findByElementId(nodes, elementId) {
271
247
  * yanıt yalnız `appliedPatch` olduğu için karşılaştırma yamadan SONRA okunan şemaya dayanır.
272
248
  */
273
249
  export function schemaOpDrops(schema, ops) {
274
- const drops = { dropped: [], droppedCount: 0, droppedCountCapped: false, notes: [] };
275
250
  if (!Array.isArray(ops))
276
- return drops;
251
+ return [];
252
+ const drops = [];
277
253
  for (const op of ops) {
278
254
  if (op.op === 'add_component') {
279
255
  const component = (op.component ?? {});
@@ -282,10 +258,12 @@ export function schemaOpDrops(schema, ops) {
282
258
  if (!elementId)
283
259
  continue;
284
260
  if (!saved) {
285
- addDrop(drops, `add_component '${elementId}': komponent şemada bulunamadı — yama 200 döndü ama düğüm kaydedilmemiş.`);
261
+ drops.push(`add_component '${elementId}': komponent şemada bulunamadı — yama 200 döndü ama düğüm kaydedilmemiş.`);
286
262
  continue;
287
263
  }
288
- mergeNested(drops, writeDiff(unwrapProps(component), unwrapProps(saved), {}, false), `add_component '${elementId}' → `);
264
+ for (const entry of silentlyDropped(unwrapProps(component), unwrapProps(saved))) {
265
+ drops.push(`add_component '${elementId}' → ${entry}`);
266
+ }
289
267
  continue;
290
268
  }
291
269
  if (op.op === 'replace_prop' && typeof op.target === 'string' && typeof op.prop === 'string') {
@@ -294,18 +272,13 @@ export function schemaOpDrops(schema, ops) {
294
272
  continue;
295
273
  const props = (saved.props ?? {});
296
274
  if (!Object.hasOwn(props, op.prop)) {
297
- addDrop(drops, `replace_prop '${op.target}.${op.prop}': prop şemada yok — değer kaydedilmemiş.`);
275
+ drops.push(`replace_prop '${op.target}.${op.prop}': prop şemada yok — değer kaydedilmemiş.`);
298
276
  continue;
299
277
  }
300
- mergeNested(drops, writeDiff({ [op.prop]: unwrapProps(op.value) }, { [op.prop]: unwrapProps(props[op.prop]) }, {}, false), `replace_prop '${op.target}' → `);
278
+ for (const entry of silentlyDropped({ [op.prop]: unwrapProps(op.value) }, { [op.prop]: unwrapProps(props[op.prop]) })) {
279
+ drops.push(`replace_prop '${op.target}' → ${entry}`);
280
+ }
301
281
  }
302
282
  }
303
- return finishReport(drops);
304
- }
305
- /** İç karşılaştırmanın satırlarını, sayısını ve notlarını dış rapora önekle taşır; kesme en dışta yapılır. */
306
- function mergeNested(target, nested, prefix) {
307
- for (const line of nested.dropped)
308
- addDrop(target, `${prefix}${line}`);
309
- target.droppedCountCapped = target.droppedCountCapped || nested.droppedCountCapped;
310
- target.notes.push(...nested.notes.map(note => `${prefix}${note}`));
283
+ return drops.slice(0, maxDrops);
311
284
  }