@appaflytech/zeyrek-ai-mcp 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist/knowledge/componentCatalog.js +9 -9
- package/dist/knowledge/componentEvents.js +1 -1
- package/dist/knowledge/rules.js +21 -0
- package/dist/platform/diagnostics.js +85 -0
- package/dist/platform/ifExists.js +129 -0
- package/dist/platform/responses.js +2 -1
- package/dist/platform/strictArguments.js +22 -2
- package/dist/platform/writeDiff.js +58 -31
- package/dist/platform/writeView.js +17 -0
- package/dist/resources/guides/functions.md +31 -2
- package/dist/resources/guides/getting-started.md +35 -0
- package/dist/resources/guides/page.md +1 -1
- package/dist/resources/guides/translation.md +4 -5
- package/dist/resources/schema-fingerprint.json +3 -3
- package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +18 -0
- package/dist/tools/designSchemaPatch.js +3 -3
- package/dist/tools/flows.js +35 -41
- package/dist/tools/functions.js +20 -10
- package/dist/tools/microservices.js +39 -11
- package/dist/tools/pages.js +55 -12
- package/dist/tools/workflows.js +39 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,10 +31,10 @@ ekleyin:
|
|
|
31
31
|
"command": "npx",
|
|
32
32
|
"args": ["-y", "@appaflytech/zeyrek-ai-mcp@latest", "--stdio"],
|
|
33
33
|
"env": {
|
|
34
|
-
"ORG_API_BASE_URL": "
|
|
35
|
-
"TENANT_API_BASE_URL": "
|
|
36
|
-
"ORGANIZATION": "
|
|
37
|
-
"HOST_SUFFIX": "
|
|
34
|
+
"ORG_API_BASE_URL": "http://localhost:17000",
|
|
35
|
+
"TENANT_API_BASE_URL": "http://localhost:15000",
|
|
36
|
+
"ORGANIZATION": "zeyrek-test",
|
|
37
|
+
"HOST_SUFFIX": "zeyrek.local",
|
|
38
38
|
"DEFAULT_ENVIRONMENT": "development",
|
|
39
39
|
"USERNAME": "admin@zeyrek.com",
|
|
40
40
|
"PASSWORD": "parolanız",
|
|
@@ -49,10 +49,10 @@ Claude Code'da komutla eklemek isterseniz:
|
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
claude mcp add zeyrek-ai \
|
|
52
|
-
-e ORG_API_BASE_URL=
|
|
53
|
-
-e TENANT_API_BASE_URL=
|
|
52
|
+
-e ORG_API_BASE_URL=http://localhost:17000 \
|
|
53
|
+
-e TENANT_API_BASE_URL=http://localhost:15000 \
|
|
54
54
|
-e ORGANIZATION=new-zeyrek \
|
|
55
|
-
-e HOST_SUFFIX=
|
|
55
|
+
-e HOST_SUFFIX=zeyrek.local \
|
|
56
56
|
-e DEFAULT_ENVIRONMENT=development \
|
|
57
57
|
-e USERNAME=admin@zeyrek.com \
|
|
58
58
|
-e PASSWORD='parolanız' \
|
|
@@ -87,9 +87,9 @@ hiç yazmayıp çalışma dizininize `PASSWORD=...` içeren bir `.env` koyun.
|
|
|
87
87
|
|
|
88
88
|
| Değişken | Örnek | Not |
|
|
89
89
|
|---|---|---|
|
|
90
|
-
| `ORG_API_BASE_URL` | `
|
|
91
|
-
| `TENANT_API_BASE_URL` | `
|
|
92
|
-
| `ORGANIZATION` | `
|
|
90
|
+
| `ORG_API_BASE_URL` | `http://localhost:17000` | Organization.API kökü |
|
|
91
|
+
| `TENANT_API_BASE_URL` | `http://localhost:15000` | Tenant.API kökü |
|
|
92
|
+
| `ORGANIZATION` | `zeyrek-test` | Org subdomain'i; istekler `Host: {ORGANIZATION}.{HOST_SUFFIX}` ile gider |
|
|
93
93
|
| `DEFAULT_ENVIRONMENT` | `development` | Gömülü varsayılanı **yoktur**; boşsa sunucu açılmaz |
|
|
94
94
|
| `USERNAME` + `PASSWORD` | | `AUTH_MODE=local` için; `/connect/token` ile takas edilir |
|
|
95
95
|
| `TOKEN` | | `USERNAME`/`PASSWORD` yerine hazır Bearer JWT verilebilir |
|
|
@@ -113,8 +113,9 @@ hiç yazmayıp çalışma dizininize `PASSWORD=...` içeren bir `.env` koyun.
|
|
|
113
113
|
| `STUDIO_BASE_URL` | — | Yalnız `page_screenshot`: Studio arayüzünün kök adresi |
|
|
114
114
|
| `SCREENSHOT_BROWSER_PATH` | — | `page_screenshot` için sistemdeki Chromium ikilisi (verilmezse playwright-core'un indirdiği tarayıcı) |
|
|
115
115
|
|
|
116
|
-
Tam liste ve yorumlu şablon: pakete dahil edilen `.env.example` (lokal profil)
|
|
117
|
-
|
|
116
|
+
Tam liste ve yorumlu şablon: pakete dahil edilen `.env.example` (lokal profil). Uzak
|
|
117
|
+
kurulumlara dağıtım insanın ve CI'ın işidir; AI lokal dışına çıkmaz
|
|
118
|
+
([../docs/ortam.md](../docs/ortam.md)).
|
|
118
119
|
|
|
119
120
|
### Bilinmesi gereken üç davranış
|
|
120
121
|
|
|
@@ -250,7 +251,7 @@ ve `src/` yoktur.
|
|
|
250
251
|
```bash
|
|
251
252
|
cd zeyrek-mcp
|
|
252
253
|
npm install
|
|
253
|
-
cp .env.example ../.env #
|
|
254
|
+
cp .env.example ../.env # hedef her zaman lokal stack
|
|
254
255
|
npm run dev # HTTP · npm run start:stdio ile stdio
|
|
255
256
|
```
|
|
256
257
|
|
|
@@ -1589,25 +1589,25 @@ export const componentKnowledge = {
|
|
|
1589
1589
|
}
|
|
1590
1590
|
},
|
|
1591
1591
|
'language-selector': {
|
|
1592
|
-
summary: 'Son kullanıcının sayfa dilini değiştirdiği açılır liste; projenin aktif dillerini kendi ana dil adlarıyla listeler.',
|
|
1592
|
+
summary: 'Son kullanıcının sayfa dilini değiştirdiği açılır liste; projenin aktif dillerini kendi ana dil adlarıyla listeler. Görsel olarak dropdown ile aynı kontroldür (ortak Select, aynı boy/tema jetonları).',
|
|
1593
1593
|
behavior: [
|
|
1594
|
-
"change olayında runtime machine.setLanguage(value) çağırır: seçim localStorage'a (zeyrek:{appSlug}:lang) yazılır ve çeviri bağlı bütün prop'lar SAYFA YENİLENMEDEN yeniden çözülür.",
|
|
1595
|
-
'Projede dil tanımlı değilse liste boş çizilir — önce proje dillerini (Translations paneli) kur. Bu durumda
|
|
1594
|
+
"change olayında runtime machine.setLanguage(value) çağırır: seçim localStorage'a (zeyrek:{appSlug}:lang) yazılır ve çeviri bağlı bütün prop'lar SAYFA YENİLENMEDEN yeniden çözülür. Olay yükü akışa params kanalından da iner, yani event.data.value akışta okunabilir.",
|
|
1595
|
+
'Projede dil tanımlı değilse liste boş çizilir (ortak Select’in boş durumu) — önce proje dillerini (Translations paneli) kur. Bu durumda kutuda yalnız placeholder görünür; placeholder varsayılan olarak BOŞ olduğu için metni yazılmadıkça kutu yine boş görünür.',
|
|
1596
1596
|
'mount olayında self.value BOŞTUR (ölçüldü, 2026-09-18): geçerli dili render katmanı her turda prop olarak enjekte ediyor ama düğümün prop kaydına yazmıyor; düğüme yazan tek şey change olayının yüküdür. Yani "açılışta dile göre dallan" kurulamıyor (docs/issues.md ARC-098), dili okuyan akış change olayına bağlanmalı.',
|
|
1597
|
-
'
|
|
1597
|
+
'Listeyi KANVAS da gösterir: tasarım anında projenin dilleri ve başlıktaki içerik dili enjekte edilir, yani bileşen tasarımcıda da dolu görünür ve displayMode/size/placeholder etkisi kanvasta ölçülür. Kanvasta liste AÇILMAZ (çift tıklama özellik panelini açsın diye); seçimi önizlemede dene.',
|
|
1598
1598
|
'Listeyi ve seçili değeri render katmanı enjekte eder; ikisi de panelden ya da akıştan YAZILAMAZ. value prop gizlidir ve tek yönlüdür: change onu düğüme geri yazar, akış self.value ile okur. Dili akıştan değiştirmenin yolu bu prop DEĞİLDİR.',
|
|
1599
1599
|
"Dil kümesi KAPALI DEĞİLDİR: projeye eklenen her ISO 639-1 kodu listede çıkar (project_update languages). Komponent kütüphanesinin kendi arayüz metni (sayfalama, filtre operatörleri, boş durumlar) yalnız tr/en sözlüğü taşır ve sözlüğü olmayan dilde İNGİLİZCEYE düşer — sayfa içeriği ve sayı/tarih biçimi ise seçilen dilin kendisini kullanır. Yani 'de' seçili bir uygulamada tablo sayfalaması İngilizce, sayılar Almanca ayraçla çıkar; bu bilinçlidir.",
|
|
1600
|
-
"Sekme başlığı (page.browserTitleKey) bu değişimi
|
|
1600
|
+
"Sekme başlığı (page.browserTitleKey) bu değişimi İZLER: dil değişince başlık da yeni dilde çözülür (ölçüldü 2026-09-11); anahtarın o dilde değeri yoksa projenin varsayılan diline düşer.",
|
|
1601
1601
|
'Seçim UYGULAMA başınadır, proje ya da sayfa başına değil: anahtar uygulamanın slug\'ını taşır (zeyrek:{appSlug}:lang), yani aynı tarayıcıda açılan başka bir uygulama kendi değerini tutar ama o uygulamanın bütün sayfaları ve önizlemeleri tek değeri paylaşır. Sayfaya birden fazla koymanın anlamı yok.'
|
|
1602
1602
|
],
|
|
1603
1603
|
propNotes: {
|
|
1604
|
-
label: '
|
|
1604
|
+
label: 'Etiket; geniş kutuda (280px üstü, kap sorgusu) listenin SOLUNDA, dar kutuda ÜSTÜNDE durur ve kontrole <label for> ile bağlıdır. Boşken (yalnız boşluk yazmak da boş sayılır) erişilebilir ad placeholder metnine, o da boşsa Language sabitine düşer.',
|
|
1605
1605
|
value: 'Gizli; runtime YAZAR (durum kanalı). Seçilen dil kodu. AI bunu set etmez — panelden ya da akıştan yazılan değer render katmanının enjeksiyonuyla ezilir.',
|
|
1606
|
-
placeholder: 'Yalnız seçili dil listede YOKKEN
|
|
1606
|
+
placeholder: 'Yalnız seçili dil listede YOKKEN kutuda görünen yer tutucu metin. Varsayılanı boştur: dil kurulmamış projede kutunun boş görünmemesi için metni SEN yazacaksın.',
|
|
1607
1607
|
description: 'Listenin altındaki yardımcı metin.',
|
|
1608
1608
|
displayMode: "Seçenek etiketinin biçimi: 'native' ana dil adı (Türkçe), 'code' büyük harf kod (TR), 'both' ikisi birden (Türkçe (TR)). Sözlükte karşılığı olmayan kod her kipte kendi kodunu gösterir.",
|
|
1609
|
-
isDisabled: '
|
|
1610
|
-
size: "Kutu ölçüsü: 'sm' | 'md' | 'lg'; boş bırakılırsa temanın form varsayılanı geçerlidir.
|
|
1609
|
+
isDisabled: 'Liste açılmaz ve Tab sırasından düşer.',
|
|
1610
|
+
size: "Kutu ölçüsü: 'sm' | 'md' | 'lg'; boş bırakılırsa temanın form varsayılanı geçerlidir. dropdown ile aynı ortak kontrol olduğundan yanına konan aynı ölçüdeki form girdileriyle aynı yükseklikte durur.",
|
|
1611
1611
|
isHidden: 'Düğümü hiç çizmez (ortak katman).'
|
|
1612
1612
|
},
|
|
1613
1613
|
guide: 'translation'
|
|
@@ -505,7 +505,7 @@ export const componentEventContracts = {
|
|
|
505
505
|
mount: { ...NO_DATA, reads: 'self.value BOŞTUR: enjekte edilen geçerli dil düğümün prop kaydına yazılmıyor, oraya yalnız change yükü yazılıyor (ölçüldü 2026-09-18, ARC-098). Dili okuyacak akışı change’e bağla' },
|
|
506
506
|
change: {
|
|
507
507
|
payload: { value: 'string (seçilen dil kodu, projenin yazdığı biçimde)' },
|
|
508
|
-
reads: 'Yük düğümün kendi value prop’una yazılır (yazmayı machine.onEmit yapar, çünkü olayda data var), akış self.value ile okur. Dili DEĞİŞTİREN yazma bu değildir: render katmanı aynı olayda machine.setLanguage(value) + setAppLocale(value) çağırır. Yalnız kullanıcı seçiminde yayılır; akışın value’ya yazması hiçbir şeyi değiştirmez (enjekte edilen değer ezer)'
|
|
508
|
+
reads: 'Yük düğümün kendi value prop’una yazılır (yazmayı machine.onEmit yapar, çünkü olayda data var), akış self.value ile okur; aynı yük params kanalından da iner, yani event.data.value da dolu. Dili DEĞİŞTİREN yazma bu değildir: render katmanı aynı olayda machine.setLanguage(value) + setAppLocale(value) çağırır. Yalnız kullanıcı seçiminde yayılır; akışın value’ya yazması hiçbir şeyi değiştirmez (enjekte edilen değer ezer)'
|
|
509
509
|
},
|
|
510
510
|
focus: NO_DATA,
|
|
511
511
|
blur: NO_DATA
|
package/dist/knowledge/rules.js
CHANGED
|
@@ -422,6 +422,8 @@ export function rawQueryParameterNames(rawQuery) {
|
|
|
422
422
|
}
|
|
423
423
|
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
424
424
|
const functionsGuide = 'ngapps://guides/functions';
|
|
425
|
+
/** FN-006: bu kadar dolu satırdan uzun gövde yorumsuz kalmaz; tek satırlık `return` yorum istemez. */
|
|
426
|
+
const FUNCTION_COMMENT_MIN_LINES = 5;
|
|
425
427
|
export const policyRules = [
|
|
426
428
|
{
|
|
427
429
|
id: 'MS-001',
|
|
@@ -2032,6 +2034,25 @@ export const policyRules = [
|
|
|
2032
2034
|
return [];
|
|
2033
2035
|
}
|
|
2034
2036
|
},
|
|
2037
|
+
{
|
|
2038
|
+
id: 'FN-006',
|
|
2039
|
+
module: 'function',
|
|
2040
|
+
appliesTo: ['create', 'update'],
|
|
2041
|
+
sourceRef: "Kullanıcı isteği 2026-09-18: ajan fonksiyon yazarken gövdeye ne yaptığını söyleyen yorum koyar; Studio editörü yorumu yeşil italik çizer. Gövde kendi satırına sarılır (function-source.ts, GlobalFunction.cs AppendLine), '//' kapanış parantezini yutmaz",
|
|
2042
|
+
check: definition => {
|
|
2043
|
+
const name = String(definition.name ?? '?');
|
|
2044
|
+
const body = typeof definition.body === 'string' ? definition.body : '';
|
|
2045
|
+
const lineCount = body.split('\n').filter(line => line.trim() !== '').length;
|
|
2046
|
+
if (lineCount < FUNCTION_COMMENT_MIN_LINES || /(^|\s)\/\/|\/\*/m.test(body))
|
|
2047
|
+
return [];
|
|
2048
|
+
return [{
|
|
2049
|
+
ruleId: 'FN-006',
|
|
2050
|
+
severity: 'warn',
|
|
2051
|
+
message: `'${name}' gövdesi ${lineCount} satır ama hiç yorum yok. Başa ne yaptığını, parametreleri ve dönüşü söyleyen kısa bir '//' yorumu, açık olmayan adımların üstüne de birer satır yorum ekleyin; kodu tekrar eden yorum yazmayın.`,
|
|
2052
|
+
guide: functionsGuide
|
|
2053
|
+
}];
|
|
2054
|
+
}
|
|
2055
|
+
},
|
|
2035
2056
|
{
|
|
2036
2057
|
id: 'FLOW-024',
|
|
2037
2058
|
module: 'flow',
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const activeMode = new AsyncLocalStorage();
|
|
4
|
+
const MAX_NOTES = 8;
|
|
5
|
+
const MAX_NOTE_CHARS = 240;
|
|
6
|
+
const MAX_DROPS = 5;
|
|
7
|
+
export const diagnosticsParam = z.enum(['compact', 'full']).optional().describe("Yanıttaki gatewayNormalized / silentlyDropped listelerinin boyu. compact (varsayılan): aynı not tekilleşir, uzun not kısalır (en fazla 8); aynı yolun düşmeleri tek satırda sayıyla toplanır (en fazla 5 grup); droppedCount her zaman tam sayıdır. full: listelerin tamamı.");
|
|
8
|
+
export function runWithDiagnostics(mode, run) {
|
|
9
|
+
return activeMode.run(mode, run);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Tanılama listeleri her yazma çağrısında yanıta girer ve aynı gerekçe cümlesi çağrı başına tekrar
|
|
13
|
+
* eder. Kırpma yalnız GÖRÜNÜMÜ küçültür: `silentlyDropped` "kaydedildi mi" sorusunun tek kanıtı olduğu
|
|
14
|
+
* için liste hiçbir zaman sessizce boşalmaz — toplam sayı ve kalan satır sayısı yanıtta kalır, tam
|
|
15
|
+
* liste gateway günlüğünde (`sessiz düşme` / `düzeltme` adımları) durur.
|
|
16
|
+
*
|
|
17
|
+
* Kip bağlamı olmayan çağrı (okuma ve silme tool'ları, doğrudan `toolResult` kullanan testler) tam
|
|
18
|
+
* listeyi görür; kısaltma yalnız `diagnostics` parametresini taşıyan yazma tool'larında devreye girer.
|
|
19
|
+
*/
|
|
20
|
+
export function compactDiagnostics(payload) {
|
|
21
|
+
if (activeMode.getStore() !== 'compact')
|
|
22
|
+
return payload;
|
|
23
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
24
|
+
return payload;
|
|
25
|
+
const record = payload;
|
|
26
|
+
const notes = Array.isArray(record.gatewayNormalized) ? record.gatewayNormalized.map(String) : undefined;
|
|
27
|
+
const drops = Array.isArray(record.silentlyDropped) ? record.silentlyDropped.map(String) : undefined;
|
|
28
|
+
if (notes === undefined && drops === undefined)
|
|
29
|
+
return payload;
|
|
30
|
+
const hidden = [];
|
|
31
|
+
const result = { ...record };
|
|
32
|
+
if (notes !== undefined) {
|
|
33
|
+
const counts = new Map();
|
|
34
|
+
for (const note of notes)
|
|
35
|
+
counts.set(note, (counts.get(note) ?? 0) + 1);
|
|
36
|
+
const unique = [...counts.entries()].map(([note, count]) => {
|
|
37
|
+
const shortened = note.length > MAX_NOTE_CHARS ? `${note.slice(0, MAX_NOTE_CHARS - 1)}…` : note;
|
|
38
|
+
return count > 1 ? `${shortened} (×${count})` : shortened;
|
|
39
|
+
});
|
|
40
|
+
const shortenedCount = notes.filter(note => note.length > MAX_NOTE_CHARS).length;
|
|
41
|
+
const kept = unique.slice(0, MAX_NOTES);
|
|
42
|
+
result.gatewayNormalized = unique.length > MAX_NOTES ? [...kept, `… ${unique.length - MAX_NOTES} not daha`] : kept;
|
|
43
|
+
if (unique.length > MAX_NOTES)
|
|
44
|
+
hidden.push(`${unique.length - MAX_NOTES} not`);
|
|
45
|
+
if (shortenedCount > 0)
|
|
46
|
+
hidden.push(`${shortenedCount} notun gerekçesi kısaltıldı`);
|
|
47
|
+
}
|
|
48
|
+
if (drops !== undefined) {
|
|
49
|
+
const droppedCount = typeof record.droppedCount === 'number' ? record.droppedCount : drops.length;
|
|
50
|
+
const grouped = groupDrops(drops, record.droppedCountCapped === true || droppedCount > drops.length);
|
|
51
|
+
const kept = grouped.slice(0, MAX_DROPS);
|
|
52
|
+
if (grouped.length !== drops.length)
|
|
53
|
+
hidden.push('benzer düşme satırları toplandı');
|
|
54
|
+
if (grouped.length > MAX_DROPS)
|
|
55
|
+
hidden.push(`${grouped.length - MAX_DROPS} düşme grubu`);
|
|
56
|
+
result.silentlyDropped = grouped.length > MAX_DROPS ? [...kept, `… ${grouped.length - MAX_DROPS} grup daha`] : kept;
|
|
57
|
+
result.droppedCount = droppedCount;
|
|
58
|
+
}
|
|
59
|
+
if (hidden.length === 0)
|
|
60
|
+
return result;
|
|
61
|
+
return { ...result, diagnosticsNote: "Kısaltıldı; yazmayı tekrarlamayın. Tam liste: sonraki çağrıda diagnostics:'full'." };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Aynı yolun farklı öğelerindeki düşmeler (`selectFields[fieldId=a].path`, `selectFields[fieldId=b].path`, …)
|
|
65
|
+
* aynı sorunu tekrar eder. Seçici ve indeks `[*]`'a indirgenir, grup tek satırda sayıyla ve bir örnekle
|
|
66
|
+
* döner. Listedeki HER satır kayıptır (sıra uyarısı ve kesme bilgisi `silentlyDroppedNotes`'tadır ve
|
|
67
|
+
* buraya hiç girmez); micro-op satırları (`add_component 'x': …`) da kendi yolu altında gruplanır.
|
|
68
|
+
* Liste kesildiyse ya da sayım tavana vurduysa grup sayısı "en az" diye yazılır.
|
|
69
|
+
*/
|
|
70
|
+
function groupDrops(drops, partial) {
|
|
71
|
+
const groups = new Map();
|
|
72
|
+
for (const line of drops) {
|
|
73
|
+
const separator = line.indexOf(': ');
|
|
74
|
+
const path = separator === -1 ? line : line.slice(0, separator);
|
|
75
|
+
const outcome = line.includes(' ↔ ') ? (line.endsWith('kaydedilen yok') ? ' · kaydedilmedi' : ' · farklı kaydedildi') : '';
|
|
76
|
+
const pattern = `${path.replace(/\[[^\]]*\]/g, '[*]')}${outcome}`;
|
|
77
|
+
groups.set(pattern, [...(groups.get(pattern) ?? []), line]);
|
|
78
|
+
}
|
|
79
|
+
return [...groups.entries()].map(([pattern, lines]) => {
|
|
80
|
+
if (lines.length === 1)
|
|
81
|
+
return lines[0];
|
|
82
|
+
const example = lines[0].length > 160 ? `${lines[0].slice(0, 159)}…` : lines[0];
|
|
83
|
+
return `${pattern}: ${partial ? 'en az ' : ''}${lines.length} öğe — ör. ${example}`;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PlatformApiError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* `ifExists` parametresi. Aynı ad ya da slug'la ikinci kaydın `"<ad> (1)"` olarak açılması platformun
|
|
5
|
+
* ürün kararıdır (akış, sayfa, workflow ve microservice servisleri aynı sayaç kuralını uygular) ve
|
|
6
|
+
* değişmez. MCP ajanı çağrıyı tekrar edebildiği için varsayılan "yazma, mevcut id'yi söyle"dir.
|
|
7
|
+
*/
|
|
8
|
+
export function ifExistsParam(noun, updateNote) {
|
|
9
|
+
return z.enum(['error', 'update', 'duplicate']).default('error').describe(`Aynı ad ya da slug'da (büyük/küçük harf duyarsız) ${noun} varsa ne olacağı. 'error' (varsayılan): yazmaz, mevcut kaydın id'sini döner. 'update': ${updateNote}. 'duplicate': platformun ürün davranışı — ikinci kayıt açılır, platform ada/slug'a ' (1)' sayacını ekler (Studio'daki 'New … (1)' ile aynı kural).`);
|
|
10
|
+
}
|
|
11
|
+
const upper = (value) => String(value ?? '').toUpperCase();
|
|
12
|
+
const isPresent = (value) => value !== undefined && value !== null && String(value) !== '';
|
|
13
|
+
/**
|
|
14
|
+
* Var-mı kapısı (FLOW-026 ve kardeşleri). Aday araması platformda harfe duyarlıdır; gateway bilinçli
|
|
15
|
+
* olarak harf duyarsız karşılaştırır. Var-mı okuması başarısız olursa çağrı düşer — okuyamadığı
|
|
16
|
+
* listeyi boş sayan kontrol tam da zaman aşımında kopya üretirdi.
|
|
17
|
+
*/
|
|
18
|
+
export async function existingMatch(options) {
|
|
19
|
+
const { code, noun, where, name, slug, id, idParam, ifExists } = options;
|
|
20
|
+
const slugCounter = (options.slugSuffix ?? 'dash') === 'dash' ? "'-1'" : "' (1)'";
|
|
21
|
+
if (ifExists === 'duplicate')
|
|
22
|
+
return undefined;
|
|
23
|
+
if (!name && !slug)
|
|
24
|
+
return undefined;
|
|
25
|
+
let records;
|
|
26
|
+
try {
|
|
27
|
+
records = await options.list();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new PlatformApiError(`${code}: mevcut ${noun} kayıtları okunamadı, ${noun} YAZILMADI (${error instanceof Error ? error.message : String(error)}). Liste okunmadan yazmak aynı adla ikinci kayıt açabilir; ${options.listTool} ile tekrar deneyin ya da bilerek ikinci kayıt istiyorsanız ifExists:'duplicate' verin.`);
|
|
31
|
+
}
|
|
32
|
+
const matches = records.filter(record => (name !== undefined && name !== '' && upper(record.name) === upper(name)) ||
|
|
33
|
+
(slug !== undefined && slug !== '' && upper(record.slug) === upper(slug)));
|
|
34
|
+
const [match] = matches;
|
|
35
|
+
if (match === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
const describe = (record) => `${String(record.id)} ('${String(record.name)}', slug '${String(record.slug)}')`;
|
|
38
|
+
const copyPattern = name
|
|
39
|
+
? new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} \\(\\d+\\)$`, 'i')
|
|
40
|
+
: undefined;
|
|
41
|
+
const copies = copyPattern ? records.filter(record => copyPattern.test(String(record.name ?? ''))) : [];
|
|
42
|
+
const copyNote = copies.length > 0
|
|
43
|
+
? ` ${where[0].toUpperCase()}${where.slice(1)} bu adın sayaçlı eşleri de var: ${copies.map(describe).join(' · ')} — ${options.copyCheck}.`
|
|
44
|
+
: '';
|
|
45
|
+
if (ifExists === 'error') {
|
|
46
|
+
throw new PlatformApiError(`${code}: ${where} aynı ad ya da slug'da ${noun} zaten var: ${matches.map(describe).join(' · ')}. İstek gönderilmedi — gönderilseydi platform ürün kuralı gereği ikinci bir ${noun} açardı (çakışan ada ' (1)', slug'a ${slugCounter} sayacı eklenerek; yalnız harf farkı varsa sayaçsız). Mevcut kaydı güncellemek için aynı çağrıyı ifExists:'update' ile tekrarlayın; bilerek ikinci kayıt istiyorsanız ifExists:'duplicate'.${copyNote}`, 409);
|
|
47
|
+
}
|
|
48
|
+
if (matches.length > 1) {
|
|
49
|
+
throw new PlatformApiError(`${code}: ad ve slug farklı kayıtlara çıkıyor (${matches.map(describe).join(' · ')}); hangisinin güncelleneceği belirsiz, istek gönderilmedi. ${options.updateTool} kullanın.${copyNote}`, 409);
|
|
50
|
+
}
|
|
51
|
+
// Eşleşme tipten bağımsızdır (platform sayacı tipe bakmaz), ama 'update' tipi farklı kaydın üstüne
|
|
52
|
+
// yazarsa kaydın türü değişir: backend PUT'ta tipi de yazar ve middleware automation'a dönüşür (R-1).
|
|
53
|
+
const kind = options.kind;
|
|
54
|
+
if (kind !== undefined && isPresent(kind.value) && isPresent(match[kind.key]) && upper(match[kind.key]) !== upper(kind.value)) {
|
|
55
|
+
throw new PlatformApiError(`${code}: aynı ad ya da slug'daki mevcut kayıt ${describe(match)} ${kind.key}='${String(match[kind.key])}'; gönderilen gövde ${kind.key}='${String(kind.value)}'. ifExists:'update' kaydın türünü değiştirirdi — istek gönderilmedi. Farklı ad/slug seçin ya da bilerek ikinci kayıt istiyorsanız ifExists:'duplicate'.${copyNote}`, 409);
|
|
56
|
+
}
|
|
57
|
+
if (id !== undefined && id.toLowerCase() !== String(match.id).toLowerCase()) {
|
|
58
|
+
throw new PlatformApiError(`${code}: verilen ${idParam} (${id}) aynı adlı mevcut kayıtla (${describe(match)}) çelişiyor; istek gönderilmedi. Mevcut kaydı güncellemek için ${idParam}'yi vermeyin ya da ${String(match.id)} verin.`, 409);
|
|
59
|
+
}
|
|
60
|
+
return match;
|
|
61
|
+
}
|
|
62
|
+
const LIST_PAGE_LENGTH = 200;
|
|
63
|
+
const LIST_PAGE_LIMIT = 50;
|
|
64
|
+
/**
|
|
65
|
+
* Uygulama düzeyindeki liste uçlarını (`GET /microservices|workflows|pages/{app}`) sonuna kadar okur.
|
|
66
|
+
* Arama terimi kullanılmaz: platformun `searchTerm`'ü slug'ı kapsamayabilir ve eksik liste kopya açar.
|
|
67
|
+
*
|
|
68
|
+
* Platform sayfaları kararlı sırada vermiyor (R-3): bir kayıt iki sayfada gelip başka biri hiç
|
|
69
|
+
* gelmeyebilir. İlk yanıttaki `meta.totalData` okunan TEKİL id sayısıyla tutmazsa liste tutarsızdır ve
|
|
70
|
+
* hata atılır — çağıran yazmaz. `meta` taşımayan eski yanıtta denetim yapılamaz.
|
|
71
|
+
*/
|
|
72
|
+
export async function listAll(platform, project, environment, path) {
|
|
73
|
+
const records = [];
|
|
74
|
+
let expected;
|
|
75
|
+
for (let pageIndex = 0; pageIndex < LIST_PAGE_LIMIT; pageIndex++) {
|
|
76
|
+
const payload = await platform.tenant('GET', project, path, { environment, query: { pageIndex, pageLength: LIST_PAGE_LENGTH } });
|
|
77
|
+
if (!Array.isArray(payload?.data))
|
|
78
|
+
throw new PlatformApiError('liste yanıtında data dizisi yok');
|
|
79
|
+
if (pageIndex === 0 && typeof payload.meta?.totalData === 'number')
|
|
80
|
+
expected = payload.meta.totalData;
|
|
81
|
+
records.push(...payload.data);
|
|
82
|
+
if (payload.data.length < LIST_PAGE_LENGTH || (expected !== undefined && records.length >= expected)) {
|
|
83
|
+
const unique = new Set(records.map(record => String(record.id).toLowerCase())).size;
|
|
84
|
+
if (expected !== undefined && unique !== expected) {
|
|
85
|
+
throw new PlatformApiError(`liste tutarsız okundu: platform ${expected} kayıt dedi, sayfalardan ${unique} tekil kayıt geldi (${records.length - unique} tekrar) — sayfa sırası kararlı değil`);
|
|
86
|
+
}
|
|
87
|
+
return records;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new PlatformApiError(`liste ${LIST_PAGE_LENGTH * LIST_PAGE_LIMIT} kaydı aştı, tamamı okunamadı`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* `ifExists:'duplicate'` yazmasında platform ada ' (n)', slug'a '-n' (akışta ' (n)') ekler. Bu fark kayıp
|
|
94
|
+
* değildir (Gözlem 1): kalıba uyan alan karşılaştırmadan ÖNCE gönderilen gövdede kaydedilen değerle
|
|
95
|
+
* değiştirilir ve `renamedByPlatform`'a gider. Kalıba uymayan fark gövdede kalır, yani yine kayıptır.
|
|
96
|
+
*/
|
|
97
|
+
export function platformRename(sent, saved, slugSuffix) {
|
|
98
|
+
const record = saved && typeof saved === 'object' && !Array.isArray(saved) ? saved : {};
|
|
99
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
100
|
+
const renamed = {};
|
|
101
|
+
const adjusted = { ...sent };
|
|
102
|
+
for (const key of ['name', 'slug']) {
|
|
103
|
+
const given = sent[key];
|
|
104
|
+
const stored = record[key];
|
|
105
|
+
if (typeof given !== 'string' || typeof stored !== 'string' || given === stored)
|
|
106
|
+
continue;
|
|
107
|
+
const pattern = key === 'slug' && slugSuffix === 'dash'
|
|
108
|
+
? new RegExp(`^${escape(given)}-\\d+$`)
|
|
109
|
+
: new RegExp(`^${escape(given)} \\(\\d+\\)$`);
|
|
110
|
+
if (!pattern.test(stored))
|
|
111
|
+
continue;
|
|
112
|
+
renamed[key] = stored;
|
|
113
|
+
adjusted[key] = stored;
|
|
114
|
+
}
|
|
115
|
+
return Object.keys(renamed).length === 0 ? { sent: adjusted } : { sent: adjusted, renamed };
|
|
116
|
+
}
|
|
117
|
+
export function withRename(payload, renamed) {
|
|
118
|
+
if (renamed === undefined || !payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
119
|
+
return payload;
|
|
120
|
+
return { ...payload, renamedByPlatform: renamed };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Yazma yanıtına `outcome` ekler; ajan kaydın yeni mi açıldığını yoksa güncellendiğini mi tek alandan okur.
|
|
124
|
+
*/
|
|
125
|
+
export function withOutcome(payload, existing) {
|
|
126
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
127
|
+
return payload;
|
|
128
|
+
return { ...payload, outcome: existing === undefined ? 'created' : 'updated' };
|
|
129
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compactDiagnostics } from './diagnostics.js';
|
|
1
2
|
import { logError, logStep } from '../observability/trace.js';
|
|
2
3
|
import { PlatformApiError, describeErrorBody } from './errors.js';
|
|
3
4
|
import { PolicyBlockError } from '../knowledge/policy.js';
|
|
@@ -139,7 +140,7 @@ function asStructured(payload) {
|
|
|
139
140
|
return { data: payload };
|
|
140
141
|
}
|
|
141
142
|
export function toolResult(payload, config, expectFailure) {
|
|
142
|
-
const pruned = prune(payload);
|
|
143
|
+
const pruned = prune(compactDiagnostics(payload));
|
|
143
144
|
const { payload: safe, masked, tokenRefs } = maskSecrets(pruned);
|
|
144
145
|
const runtimeErrors = runtimeErrorSummary(safe);
|
|
145
146
|
if (runtimeErrors.length > 0) {
|
|
@@ -1,4 +1,15 @@
|
|
|
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
|
+
]);
|
|
2
13
|
function isRawShape(value) {
|
|
3
14
|
if (!value || typeof value !== 'object' || '_def' in value)
|
|
4
15
|
return false;
|
|
@@ -19,14 +30,23 @@ export function rejectUnknownArguments(server) {
|
|
|
19
30
|
server.registerTool = ((name, config, callback) => {
|
|
20
31
|
if (!isRawShape(config.inputSchema))
|
|
21
32
|
return registerTool(name, config, callback);
|
|
22
|
-
|
|
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;
|
|
23
43
|
const accepted = Object.keys(shape).join(', ');
|
|
24
44
|
const inputSchema = z.object(shape, {
|
|
25
45
|
errorMap: (issue, context) => issue.code === 'unrecognized_keys'
|
|
26
46
|
? { 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}.` }
|
|
27
47
|
: { message: context.defaultError }
|
|
28
48
|
}).strict();
|
|
29
|
-
return registerTool(name, { ...config, inputSchema },
|
|
49
|
+
return registerTool(name, { ...config, inputSchema }, run);
|
|
30
50
|
});
|
|
31
51
|
return server;
|
|
32
52
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
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;
|
|
4
5
|
const maxDepth = 12;
|
|
5
6
|
const missing = Symbol('missing');
|
|
6
7
|
/**
|
|
7
8
|
* Platformun kendi doldurduğu alanlar. Yalnız gönderilen tarafta da varsalar (patch yolunda gövde
|
|
8
9
|
* okunup geri yazılır) fark üretirlerdi ve bu fark hep gürültüdür: `audit.updatedAt` her yazmada
|
|
9
|
-
* değişir. 09-05 koşusunda `microservice_patch` yanıtındaki iki satırdan biri buydu.
|
|
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.
|
|
10
12
|
*/
|
|
11
13
|
const serverManagedKeys = new Set([
|
|
12
|
-
'audit', 'createdAt', 'updatedAt', 'createdUserId', 'createdUserName', 'updatedUserId', 'updatedUserName'
|
|
14
|
+
'audit', 'createdAt', 'updatedAt', 'createdUserId', 'createdUserName', 'updatedUserId', 'updatedUserName', 'version'
|
|
13
15
|
]);
|
|
14
16
|
/**
|
|
15
17
|
* Platformun KENDİ ürettiği kimlik. Gönderilen id kaydedilenden farklıysa bu bir kayıp değildir:
|
|
@@ -50,6 +52,29 @@ export const unstoredKeys = new Set(['fieldSlug']);
|
|
|
50
52
|
const canonicalValues = {
|
|
51
53
|
type: [['string', 'text']]
|
|
52
54
|
};
|
|
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
|
+
}
|
|
53
78
|
/**
|
|
54
79
|
* Yolun son alan adı; `tags[0]` ya da `fields[fieldId=x]` gibi indeks/anahtar ekini atar.
|
|
55
80
|
*/
|
|
@@ -81,11 +106,11 @@ function lookup(saved, key) {
|
|
|
81
106
|
return Object.hasOwn(record, key) ? record[key] : missing;
|
|
82
107
|
}
|
|
83
108
|
function compare(sent, saved, path, sink, depth) {
|
|
84
|
-
if (sink.
|
|
109
|
+
if (sink.droppedCountCapped || depth > maxDepth)
|
|
85
110
|
return;
|
|
86
111
|
if (Array.isArray(sent)) {
|
|
87
112
|
if (saved === missing) {
|
|
88
|
-
sink
|
|
113
|
+
addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
|
|
89
114
|
return;
|
|
90
115
|
}
|
|
91
116
|
const key = Array.isArray(saved) ? identityKeyFor(sent) : undefined;
|
|
@@ -103,18 +128,18 @@ function compare(sent, saved, path, sink, depth) {
|
|
|
103
128
|
}
|
|
104
129
|
return;
|
|
105
130
|
}
|
|
106
|
-
const before = sink.
|
|
131
|
+
const before = sink.droppedCount;
|
|
107
132
|
sent.forEach((entry, index) => compare(entry, lookup(saved, index), `${path}[${index}]`, sink, depth + 1));
|
|
108
133
|
// Sıra tabanlı karşılaştırmada "değişmiş" görünen öğe aslında yer değiştirmiş olabilir. Tek
|
|
109
134
|
// elemanlı dizide sıra farkı imkânsız olduğu için not düşülmez (2026-09-06 · S-1).
|
|
110
|
-
if (Array.isArray(saved) && sent.length > 1 && saved.length > 1 && sink.
|
|
111
|
-
sink.
|
|
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`);
|
|
112
137
|
}
|
|
113
138
|
return;
|
|
114
139
|
}
|
|
115
140
|
if (sent && typeof sent === 'object') {
|
|
116
141
|
if (saved === missing) {
|
|
117
|
-
sink
|
|
142
|
+
addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
|
|
118
143
|
return;
|
|
119
144
|
}
|
|
120
145
|
for (const [key, entry] of Object.entries(sent)) {
|
|
@@ -130,7 +155,7 @@ function compare(sent, saved, path, sink, depth) {
|
|
|
130
155
|
if (unstoredKeys.has(field) && (saved === missing || saved === null))
|
|
131
156
|
return;
|
|
132
157
|
if (saved === missing) {
|
|
133
|
-
sink
|
|
158
|
+
addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
|
|
134
159
|
return;
|
|
135
160
|
}
|
|
136
161
|
if (sent === saved)
|
|
@@ -141,7 +166,7 @@ function compare(sent, saved, path, sink, depth) {
|
|
|
141
166
|
sink.normalized.push(`${path}: gönderilen ${render(sent)} platformun kanonik adıyla ${render(saved)} olarak saklandı — eşdeğer, kayıp değil`);
|
|
142
167
|
return;
|
|
143
168
|
}
|
|
144
|
-
sink
|
|
169
|
+
addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen ${render(saved)}`);
|
|
145
170
|
}
|
|
146
171
|
/**
|
|
147
172
|
* Sol taraf platforma GİDEN gövdedir (gateway normalizasyonu uygulandıktan sonrası), sağ taraf
|
|
@@ -152,8 +177,8 @@ function compare(sent, saved, path, sink, depth) {
|
|
|
152
177
|
* İki liste döner: `dropped` gerçekten kaybolan alanlar, `normalized` platformun kanonik adıyla
|
|
153
178
|
* sakladığı eşdeğerler. İkincisi `gatewayNormalized`a gider, uyarıya değil.
|
|
154
179
|
*/
|
|
155
|
-
export function writeDiff(sent, saved, aliases = {}) {
|
|
156
|
-
const sink =
|
|
180
|
+
export function writeDiff(sent, saved, aliases = {}, finish = true) {
|
|
181
|
+
const sink = emptyWriteDiff();
|
|
157
182
|
if (!sent || typeof sent !== 'object')
|
|
158
183
|
return sink;
|
|
159
184
|
const wrapper = saved && typeof saved === 'object' && !Array.isArray(saved)
|
|
@@ -171,19 +196,16 @@ export function writeDiff(sent, saved, aliases = {}) {
|
|
|
171
196
|
if (keys.length > 0 && shared.length === 0)
|
|
172
197
|
return sink;
|
|
173
198
|
compare(comparable, root, '', sink, 0);
|
|
174
|
-
|
|
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;
|
|
199
|
+
return finish ? finishReport(sink) : sink;
|
|
178
200
|
}
|
|
179
201
|
export function silentlyDropped(sent, saved, aliases = {}) {
|
|
180
202
|
return writeDiff(sent, saved, aliases).dropped;
|
|
181
203
|
}
|
|
182
204
|
export function withWriteDiff(payload, diff) {
|
|
183
|
-
const drops =
|
|
184
|
-
const normalized =
|
|
205
|
+
const drops = diff.dropped;
|
|
206
|
+
const normalized = diff.normalized ?? [];
|
|
185
207
|
if (drops.length > 0) {
|
|
186
|
-
logStep('sessiz düşme ⚠', `${
|
|
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]);
|
|
187
209
|
}
|
|
188
210
|
if (normalized.length > 0) {
|
|
189
211
|
logStep('düzeltme', `platform ${normalized.length} alanı kanonik adıyla sakladı — eşdeğer, yanıta gatewayNormalized olarak eklenir`, normalized);
|
|
@@ -195,7 +217,9 @@ export function withWriteDiff(payload, diff) {
|
|
|
195
217
|
const existing = payload.gatewayNormalized;
|
|
196
218
|
return {
|
|
197
219
|
...payload,
|
|
198
|
-
...(drops.length > 0 ? { silentlyDropped: drops } : {}),
|
|
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 } : {}),
|
|
199
223
|
...(normalized.length > 0
|
|
200
224
|
? { gatewayNormalized: [...(Array.isArray(existing) ? existing : []), ...normalized] }
|
|
201
225
|
: {})
|
|
@@ -247,9 +271,9 @@ function findByElementId(nodes, elementId) {
|
|
|
247
271
|
* yanıt yalnız `appliedPatch` olduğu için karşılaştırma yamadan SONRA okunan şemaya dayanır.
|
|
248
272
|
*/
|
|
249
273
|
export function schemaOpDrops(schema, ops) {
|
|
274
|
+
const drops = { dropped: [], droppedCount: 0, droppedCountCapped: false, notes: [] };
|
|
250
275
|
if (!Array.isArray(ops))
|
|
251
|
-
return
|
|
252
|
-
const drops = [];
|
|
276
|
+
return drops;
|
|
253
277
|
for (const op of ops) {
|
|
254
278
|
if (op.op === 'add_component') {
|
|
255
279
|
const component = (op.component ?? {});
|
|
@@ -258,12 +282,10 @@ export function schemaOpDrops(schema, ops) {
|
|
|
258
282
|
if (!elementId)
|
|
259
283
|
continue;
|
|
260
284
|
if (!saved) {
|
|
261
|
-
drops
|
|
285
|
+
addDrop(drops, `add_component '${elementId}': komponent şemada bulunamadı — yama 200 döndü ama düğüm kaydedilmemiş.`);
|
|
262
286
|
continue;
|
|
263
287
|
}
|
|
264
|
-
|
|
265
|
-
drops.push(`add_component '${elementId}' → ${entry}`);
|
|
266
|
-
}
|
|
288
|
+
mergeNested(drops, writeDiff(unwrapProps(component), unwrapProps(saved), {}, false), `add_component '${elementId}' → `);
|
|
267
289
|
continue;
|
|
268
290
|
}
|
|
269
291
|
if (op.op === 'replace_prop' && typeof op.target === 'string' && typeof op.prop === 'string') {
|
|
@@ -272,13 +294,18 @@ export function schemaOpDrops(schema, ops) {
|
|
|
272
294
|
continue;
|
|
273
295
|
const props = (saved.props ?? {});
|
|
274
296
|
if (!Object.hasOwn(props, op.prop)) {
|
|
275
|
-
drops
|
|
297
|
+
addDrop(drops, `replace_prop '${op.target}.${op.prop}': prop şemada yok — değer kaydedilmemiş.`);
|
|
276
298
|
continue;
|
|
277
299
|
}
|
|
278
|
-
|
|
279
|
-
drops.push(`replace_prop '${op.target}' → ${entry}`);
|
|
280
|
-
}
|
|
300
|
+
mergeNested(drops, writeDiff({ [op.prop]: unwrapProps(op.value) }, { [op.prop]: unwrapProps(props[op.prop]) }, {}, false), `replace_prop '${op.target}' → `);
|
|
281
301
|
}
|
|
282
302
|
}
|
|
283
|
-
return drops
|
|
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}`));
|
|
284
311
|
}
|