@appaflytech/zeyrek-ai-mcp 0.0.2 → 0.0.4

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/.env.example +21 -7
  2. package/.env.production.example +42 -0
  3. package/README.md +14 -7
  4. package/dist/config.js +113 -46
  5. package/dist/knowledge/componentCatalog.js +476 -32
  6. package/dist/knowledge/componentEvents.js +154 -12
  7. package/dist/knowledge/errorPath.js +64 -0
  8. package/dist/knowledge/normalize.js +12 -1
  9. package/dist/knowledge/outlines.js +9 -2
  10. package/dist/knowledge/rules.js +184 -3
  11. package/dist/knowledge/styleResolve.js +485 -0
  12. package/dist/observability/trace.js +4 -0
  13. package/dist/platform/http.js +3 -0
  14. package/dist/platform/responses.js +80 -4
  15. package/dist/platform/screenshot.js +106 -0
  16. package/dist/resources/examples/layout-app-shell.json +368 -0
  17. package/dist/resources/examples/microservice-table-list.json +1 -0
  18. package/dist/resources/examples/middleware-authenticate.json +243 -47
  19. package/dist/resources/examples/workflow-basic.json +24 -2
  20. package/dist/resources/guides/component.md +6 -3
  21. package/dist/resources/guides/entity.md +33 -1
  22. package/dist/resources/guides/external-component.md +129 -0
  23. package/dist/resources/guides/layout.md +147 -0
  24. package/dist/resources/guides/microservice.md +35 -0
  25. package/dist/resources/guides/middleware.md +10 -0
  26. package/dist/resources/guides/page.md +137 -15
  27. package/dist/resources/guides/translation.md +80 -0
  28. package/dist/resources/guides/workflow.md +127 -2
  29. package/dist/server.js +4 -0
  30. package/dist/tools/externalComponents.js +285 -0
  31. package/dist/tools/layouts.js +48 -6
  32. package/dist/tools/microservices.js +2 -2
  33. package/dist/tools/pages.js +98 -30
  34. package/dist/tools/platformInfo.js +2 -2
  35. package/dist/tools/projects.js +7 -2
  36. package/dist/tools/screens.js +36 -0
  37. package/dist/tools/translations.js +37 -0
  38. package/dist/tools/workflows.js +3 -3
  39. package/package.json +4 -1
@@ -0,0 +1,80 @@
1
+ # Çoklu Dil Rehberi (page/content translations)
2
+
3
+ Sayfa içeriğini (metin, buton etiketi, placeholder…) çoklu dilli yapan özellik. Studio panelinin
4
+ kendi arayüz dili (`i18n.md`, yalnız frontend'i ilgilendirir) ile **karıştırma** — bu rehber
5
+ **son kullanıcının gördüğü sayfa metnini** kapsar. Kaynak: `TranslationsController.cs`
6
+ (`zeyrek-backend/src/NgApps.Tenant.API/Controllers/`) + `zeyrek-frontend/docs/features/page-translations.md`.
7
+
8
+ ## Model
9
+
10
+ - Çeviri kaydı **application başına** tutulur (`(applicationId, language, key)` başına bir satır);
11
+ aynı application'daki tüm sayfalar aynı anahtar kümesini paylaşır.
12
+ - Bir kayıt isteğe bağlı bir **`componentId`**'ye bağlanabilir — o komponent silinince kaydı da
13
+ kademeli silinir. Salt organizasyon amaçlıdır; runtime çözümlemeyi etkilemez (çözümleme yalnız
14
+ `key`'e bakar).
15
+ - Proje **dilleri** ve **varsayılan dil** application değil **proje** düzeyinde ayardır
16
+ (`project_get`/`project_update` `languages`/`defaultLanguage`) ve tam **ISO 639-1** kod
17
+ kümesinden seçilir (`ProjectConstants.SupportedLanguages`, ~184 kod). `translation_update`'e
18
+ proje dillerinde olmayan bir kod verirsen platform isteği reddeder — önce `project_update` ile
19
+ dili ekle.
20
+
21
+ ## Araçlar
22
+
23
+ | Tool | Ne yapar |
24
+ |---|---|
25
+ | `translation_list` | Application'ın tüm `(language, key, value)` kayıtlarını düz liste döner |
26
+ | `translation_update` | `keys` (ekle/güncelle, `componentId` opsiyonel) ve/veya `deletedKeys` (sil) — ikisi de aynı çağrıda verilebilir, en az biri dolu olmalı |
27
+ | `project_update` | `languages`/`defaultLanguage` — proje dillerini kurar/değiştirir |
28
+
29
+ ```jsonc
30
+ // yeni anahtar aç / var olanı güncelle
31
+ { "applicationId": "<app id>", "keys": [
32
+ { "key": "home.welcome", "values": { "tr": "Hoş geldiniz", "en": "Welcome" } }
33
+ ] }
34
+
35
+ // bir anahtarı sil, bir başkasını aynı çağrıda güncelle
36
+ { "applicationId": "<app id>",
37
+ "keys": [{ "key": "home.title", "values": { "tr": "Ana sayfa" } }],
38
+ "deletedKeys": ["home.old_banner"] }
39
+ ```
40
+
41
+ ## Bir prop'u çeviriye bağlamak
42
+
43
+ Sayfa şemasında (`platform_guide('page')` Kural 1) çeviriye bağlı bir prop **Prop nesnesinin**
44
+ `type:'translation'` varyantıdır — `PropValueType.Translation` (`DesignComponentProperty.cs`)
45
+ backend'de zaten tanımlı, Studio'nun "Translate" aksiyonunun ürettiği şekil budur:
46
+
47
+ ```jsonc
48
+ { "op": "replace_prop", "target": "welcome_text", "prop": "text",
49
+ "value": { "type": "translation", "ref": "home.welcome", "data": "Hoş geldiniz" } }
50
+ ```
51
+
52
+ - **Anahtar `ref`'tedir, `data` DEĞİL.** Runtime her `getNodeProps` turunda `data`'yı
53
+ `resolveTranslation(...)` sonucuyla EZER (`code` prop'larıyla aynı davranış — `page.md` Kural 1c
54
+ bunun `code` karşılığını anlatır). `ref`'i okumadan/yazmadan `data`'ya güvenme; `data` yalnız
55
+ Studio'nun sakladığı önceki statik değerdir (fallback/geri dönüş amaçlı, çözümlemeyi ETKİLEMEZ).
56
+ - **Fallback zinciri:** aktif dil → projenin varsayılan dili → anahtarın kendisi. Boş string de
57
+ "eksik" sayılır ve zincirde bir alt basamağa düşer — bir anahtar hiçbir zaman boş render olmaz.
58
+ - Hangi prop'lar çeviriye bağlanabilir platformca kısıtlanmaz (Studio'da yalnız `text`/`long-text`
59
+ tipli prop'larda "Translate" aksiyonu gösterilir, ama şema düzeyinde her static prop aynı Prop
60
+ şeklini kabul eder).
61
+
62
+ ## Dil değiştirme: `language-selector`
63
+
64
+ Son kullanıcı dili `language-selector` komponentiyle değiştirir (`platform_guide('component')`
65
+ + `componentCatalog` içindeki `language-selector` kaydı): `change` olayı `machine.setLanguage(value)`
66
+ çağırır, seçim `localStorage`'a (`zeyrek:{appSlug}:lang`) yazılır ve çeviriye bağlı **bütün**
67
+ prop'lar sayfa yenilenmeden yeniden çözülür — ayrı bir akış kurmana gerek yok, komponenti sayfaya
68
+ eklemen yeterli. Projede dil tanımlı değilse (`languages` boşsa) liste boş çizilir.
69
+
70
+ ## Bilinen sınırlar
71
+
72
+ - **Flow adımlarında Translation kaynağı YOK.** `value.type` sözlüğünde (`platform_guide('page')`
73
+ Kural 1c sonundaki tablo) `translation` yoktur — bir flow adımının **değerini** doğrudan bir
74
+ çeviri anahtarına bağlayamazsın, yalnız sayfa şemasındaki statik prop'lar bağlanabilir. İfadeden
75
+ okumak gerekiyorsa `t('anahtar')` Jexl fonksiyonu kullanılır (`platform_guide('expressions')`).
76
+ - **Runtime sayfa yanıtı sunucu tarafında önbelleklidir** (Postgres `caches` tablosu). Bir
77
+ `translation_update`/`project_update(languages)` çağrısından hemen sonra `page_runtime_get`
78
+ eski değerleri gösterebilir — önbellek süresi dolana kadar. Acil doğrulama gerekiyorsa
79
+ `translation_list` ile yazma tarafını, `page_runtime_get` ile okuma tarafını AYRI doğrula.
80
+ - Mobil runtime (`apps/mobile`) bu özelliği henüz beslemiyor — yalnız web.
@@ -2,6 +2,65 @@
2
2
 
3
3
  Buradaki kurallar gerçek projelerde (qm, hr) **deneyerek doğrulandı**.
4
4
 
5
+ ## Her adım NE İŞE YARADIĞINI yazar (WF-024, block)
6
+
7
+ Studio kanvası adımın **adını** gösterir, ne yaptığını göstermez. 11 satırlık bir `code` adımının
8
+ işini anlamak için ifadeyi satır satır okumak gerekir — akışı devralan geliştirici için bu, her
9
+ seferinde tersine mühendisliktir. Bu yüzden **her adım** ve **akışın kendisi** bir `description`
10
+ taşır; gateway açıklamasız gövdeyi platforma göndermez.
11
+
12
+ ```jsonc
13
+ { "name": "anket-kaydet", "type": "workflow",
14
+ "description": "Anket başlığını ve sorularını tek çağrıda kaydeder.",
15
+ "steps": [
16
+ { "id": "<GUID>", "name": "createSurvey", "type": "table",
17
+ "description": "Anket başlığını surveys'e yazar; oluşan id sonraki adımlarda kullanılır.",
18
+ "rule": { … } }
19
+ ] }
20
+ ```
21
+
22
+ - **Mekanizmayı değil NİYETİ yaz.** `"assignments tablosunu status='Aktif' ile okur"` koddan zaten
23
+ okunuyor ve hiçbir şey eklemiyor. `"kullanıcının asal pozisyonunu seçer; birden çok aktif atama
24
+ varsa is_primary olanı tercih eder, o da yoksa ilkini alır"` — yazılmadıkça hiçbir yerde
25
+ bulunmayan bilgi budur.
26
+ - **`start` ve `end` muaftır** (kural onları hiç sormaz): ikisi de yapısal işaretçi, açıklanacak
27
+ davranışları yok.
28
+ - **Gateway'in enjekte ettiği `gatewayOnError` adımı kendi açıklamasını taşır** — o adımı sen
29
+ yazmadığın için kural seni onun yüzünden durdurmaz.
30
+ - Alan **kalıcıdır ve çalışma zamanını etkilemez**: motor okumaz, yalnız Studio'da kanvas
31
+ düğümünde ⓘ ikonu + popover olarak ve adım panelinde düzenlenebilir metin olarak görünür.
32
+ İnsan da düzeltebilir.
33
+ - `preview` bu kuralın dışındadır — kaydedilmeyen bir denemede açıklama aranmaz.
34
+ - Aynı disiplin ucun kendisi için de geçerli: `platform_guide('microservice')` §MS-014.
35
+
36
+ ## Tuzağı varsa `caution` yaz (opsiyonel, kural yok)
37
+
38
+ `description` "bu adım ne işe yarar" sorusunu cevaplar. `caution` bambaşka bir soruyu cevaplar:
39
+ **"burada neye dikkat etmek gerekiyor."** İkisi ayrı alandır çünkü tek metne sıkıştırıldıklarında
40
+ ikisi de okunmaz olur — açıklama uyarı listesine döner, uyarı da açıklamanın içinde kaybolur.
41
+
42
+ **Bu alanı hiçbir kural zorlamaz ve doldurma metin kabul edilmez.** Adımın gerçek bir tuzağı
43
+ yoksa alanı hiç yazma. Yazmaya değer olanlar:
44
+
45
+ - **İdempotent değil** — "iki kez çalışırsa çift atama yazar; çağıran tarafta koruma yok"
46
+ - **Kapsam bu adımda değil** — "tenant filtresi burada yok, kimlik kapsaması bağlı middleware'de"
47
+ - **Dış bağımlılık** — "ödeme sağlayıcısına senkron çağrı; sağlayıcı yavaşlarsa istek bekler"
48
+ - **Sessiz düşen dal** — "eşleşme bulunamazsa hata dönmez, boş dizi döner"
49
+
50
+ ```jsonc
51
+ { "id": "<GUID>", "name": "createAssignment", "type": "table",
52
+ "description": "Kullanıcıyı seçilen pozisyona atar.",
53
+ "caution": "İki kez çalışırsa çift atama yazar; çağıran tarafta idempotency koruması yok." }
54
+ ```
55
+
56
+ **Yazılmayacaklar:** `"bu adım veriyi okur, dikkat"` gibi mekanizma tekrarı, `description`'ın
57
+ başka kelimelerle yazılmışı, `"dikkatli olun"` gibi genel tavsiye. Bunlar kanvastaki ⚠ işaretini
58
+ değersizleştirir — işaretin tüm değeri **seyrek** olmasından gelir.
59
+
60
+ Alan `description` gibi inerttir: motor okumaz. Studio'da kanvas düğümünde kehribar ⚠ ikonu,
61
+ adım panelinde düzenlenebilir metin, analiz belgesinde ayrı satır olarak görünür; `workflow_get`
62
+ outline görünümünde geri okunur — sonraki oturumda tuzağı tam gövdeyi almadan görürsün.
63
+
5
64
  ## Karar kuralları
6
65
 
7
66
  1. **DTO önce (yalnız workflow tipinde).** `type='workflow'` için girdi/çıktı sözleşmesini
@@ -49,6 +108,55 @@ Bağlanmamış step çalışmaz. `start` tek branch taşır; normal step'ler `su
49
108
  `condition` koşul branch'leri + `else`; terminal step'ler (`response`, `convert`, `revert`,
50
109
  `end`) **branch taşımaz**.
51
110
 
111
+ ## Hata yolu
112
+
113
+ **Gateway workflow ve middleware tanımlarına hata yolunu otomatik ekler** (yanıtta
114
+ `gatewayNormalized`): start adımına `type: 'global-error'` dalı + 500 dönen bir `response` adımı
115
+ (`gatewayOnError`). Kendi hata gövdeni istiyorsan bu adımın `bodyExpression`'ını değiştir ya da
116
+ tanımı kendi global-error dalınla gönder — dal **bağlıysa** (`nextStepId` doluysa) gateway
117
+ dokunmaz. `nextStepId: null` ile bağlanmamış gönderirsen gateway ikinci bir dal eklemez, mevcut
118
+ dalını enjekte ettiği hata adımına bağlar.
119
+
120
+ **Fail dallarını tek tek bağlamana gerek yok.** Bir adımın `fail` dalı bağlanmamışsa
121
+ (`nextStepId: null`) ya da hiç yoksa, hata **kendiliğinden** global-error dalına gider. `fail`
122
+ dalını yalnız o adıma özel bir telafi akışı (örn. "kayıt yoksa oluştur") istiyorsan bağla.
123
+
124
+ Global-error dalında `error` değişkeni tanımlıdır:
125
+
126
+ ```jsonc
127
+ { "code": "RowAlreadyExists", "message": "…", "detail": "…", "type": "…",
128
+ "step": { "id": "…", "name": "writeRow", "type": "table", "data": …, "input": [ … ] } }
129
+ ```
130
+
131
+ `error.step` hangi adımın patladığını söyler — hata yanıtına koymadan önce iki kez düşün, iç
132
+ yapıyı dışarı sızdırır.
133
+
134
+ **Kurallar ve sınırlar:**
135
+
136
+ - **Global-error dalı TEK olmalı ve START adımında durmalı.** Başka adıma koyulan dal hiç
137
+ çalışmaz, platform 400 döner. Gateway bunu `WF-022` ile istek gitmeden reddeder.
138
+ - **Hata `response` adımı ReturnDto'ya bağlı değildir.** Global-error modunda platform sözleşmeyi
139
+ atlar, gövde serbesttir — `returnDtoId` ile çakışmaz.
140
+ - **Loop içi hatalar global-error dalına GİTMEZ.** Loop çocuk bağlamı bu yönlendirmeyi kapatır;
141
+ loop adımının kendi `fail` dalını bağla.
142
+ - **`automation` tipinde `response` adımı yasaktır**, bu yüzden gateway bu tipe enjeksiyon
143
+ yapmaz. Hata yolunu kendin yaz: global-error dalını `revert` (tetikleyen satır işlemini geri
144
+ alır) ya da `email`/`table` adımına bağla. Bağlı hata yolu yoksa `WF-021` uyarır.
145
+ - **Hata yolu hiç yoksa** akış patladığında platform 500 + ham exception mesajı döner. Çalışır,
146
+ ama çıktı denetimsizdir ve lokalize değildir.
147
+
148
+ > **Preview HTTP 200 dönerken bile akış patlamış olabilir.** `executionSuccess` yalnız SON adıma
149
+ > bakar; ortada patlayıp hata dalından dönen akış `true` gösterir. Gateway tüm adımları (iç içe
150
+ > `children` ve tetiklenen `automations` dâhil) tarar ve hata bulursa yanıta `runtimeErrors`
151
+ > alanını ekler — `workflow_preview` ve `automation_preview` yanıtlarında bu alanı kontrol et.
152
+ >
153
+ > **`runtimeErrors` her tool'da aynı şeyi ölçmez.** Alan yalnız `steps[]` taşıyan yanıtlarda adım
154
+ > adım tarama yapar. `microservice_execute` / `microservice_debug` / `microservice_preview`
155
+ > yanıtlarında gateway sadece `httpStatus >= 400` durumunu bulgu sayar: bu tool'lar adım ağacını
156
+ > `executionItems` olarak döner ve o düğümlerde hata işareti yoktur, yani HTTP 200 gövdesine gömülü
157
+ > bir microservice hatası `runtimeErrors` üretmez. Orada `body`'yi kendin oku ve şüpheliyse
158
+ > `microservice_debug` ile adım adım bak. `query_preview` yanıtları hiç kapsanmaz.
159
+
52
160
  ## Condition step kuralları
53
161
 
54
162
  Koşullar `rule.items[]` dizisindedir ve **sırayla** değerlendirilir; ilk `true` olan dala
@@ -151,6 +259,11 @@ Tek uçta "başlık + N alt kayıt" yazmanın yolu budur (anket + sorular + seç
151
259
 
152
260
  - `rule` içinde `datasourceId` + `entityId` zorunlu; `selectFields`/`filters` öğelerinde
153
261
  **`fieldId` zorunlu** (yalnız `fieldSlug` yetmez → `EvaluateWorkflowStepFailedException`).
262
+ - **`selectFields` = akışın gerçekten OKUDUĞU alanlar.** Entity'nin tüm alanlarını işaretlemek
263
+ her satırı gereksiz büyütür (join'li adımda maliyet katlanır) ve adımın ne ürettiğini
264
+ okunamaz hâle getirir. Sonraki adımların, filtrelerin ve response `bodyExpression`'ının
265
+ dokunmadığı alanları çıkarın — gateway bunları **WF-023 (warn)** ile adıyla sayar. Çıktıyı
266
+ bütün olarak geçiriyorsanız (`{ list: rows.data }`) kural susar.
154
267
  - **Table step JOIN kurabilir** ve alias/path disiplini microservice'inkiyle **birebir aynıdır**
155
268
  (Studio aynı form bileşenlerini kullanır): `joins[].path` = `<kökEntityId>.<ilişkiAlanı…>`,
156
269
  join'li `selectFields[].path` = kök id olmadan aynı zincir, kök alanlarda `path` yok.
@@ -243,12 +356,15 @@ zorunludur (`CreateWorkflowCommandValidator`: `When(ReturnDtoId.HasValue) → Re
243
356
  ```jsonc
244
357
  {
245
358
  "name": "Product Lookup", "slug": "product-lookup", "type": "workflow",
359
+ "description": "Ada göre tek ürün getirir; bulunamazsa hata yoluna düşer.",
246
360
  "parameterDtoId": "<girdi DTO id>", "parameterBehavior": "object",
247
361
  "returnDtoId": "<çıktı DTO id>", "returnBehavior": "object",
248
362
  "steps": [
249
363
  { "id": "<GUID-1>", "name": "Start", "type": "start",
250
- "branches": [{ "id": "<GUID-2>", "name": "start", "type": "success", "nextStepId": "<GUID-3>" }] },
364
+ "branches": [{ "id": "<GUID-2>", "name": "start", "type": "success", "nextStepId": "<GUID-3>" },
365
+ { "id": "<GUID-7>", "name": "global-error", "type": "global-error", "nextStepId": "<GUID-8>" }] },
251
366
  { "id": "<GUID-3>", "name": "load_product", "type": "table",
367
+ "description": "input.productName ile ürünü okur; tek kayıt bekler.",
252
368
  "rule": { "stepType": "table", "datasourceId": "<datasource id>", "entityId": "<entity id>",
253
369
  "operation": "get", "isSingle": true,
254
370
  "selectFields": [{ "alias": "<entity slug>", "fieldId": "<alan id>", "fieldSlug": "name" }],
@@ -258,9 +374,16 @@ zorunludur (`CreateWorkflowCommandValidator`: `When(ReturnDtoId.HasValue) → Re
258
374
  "limit": { "name": "limit", "offset": "offset", "source": "query-string" } },
259
375
  "branches": [{ "id": "<GUID-4>", "name": "success", "type": "success", "nextStepId": "<GUID-5>" },
260
376
  { "id": "<GUID-6>", "name": "fail", "type": "fail", "nextStepId": null }] },
377
+ // ^ bağlanmamış fail dalı: hata global-error dalına gider
261
378
  { "id": "<GUID-5>", "name": "product_response", "type": "response",
379
+ "description": "Bulunan ürünün adını 200 ile döner.",
262
380
  "rule": { "bodyExpression": "{ name: load_product.data.name }", "httpStatusCode": 200,
263
- "headerExpression": "{ }" }, "branches": [] }
381
+ "headerExpression": "{ }" }, "branches": [] },
382
+ { "id": "<GUID-8>", "name": "onError", "type": "response",
383
+ "description": "Akış patlarsa hata kodunu ve mesajını 500 ile döner.",
384
+ "rule": { "stepType": "response", "httpStatusCode": 500,
385
+ "bodyExpression": "({ code: error.code, message: error.message })" },
386
+ "branches": [] }
264
387
  ]
265
388
  }
266
389
  ```
@@ -315,6 +438,8 @@ zorunludur (`CreateWorkflowCommandValidator`: `When(ReturnDtoId.HasValue) → Re
315
438
  - Automation'da `isQueue`/`operation` eksik ya da returnDtoId verilmiş (WF-010, block)
316
439
  - `fields` ∩ `selectFields` çakışması (WF-012, warn) · join'li table'da boş selectFields (WF-007, warn)
317
440
  - Join'li table step'inde kök `rule.alias` boş (WF-019, block — Postgres 42601'in sebebi)
441
+ - Akışta hiç okunmayan `selectFields` alanı (WF-023, warn — gereksiz taşınan sütun)
442
+ - Adımda ya da akışta `description` yok (WF-024, block — Studio'da adım okunamaz kalır)
318
443
  - İlişki alanının içini okuyan ifade (WF-020, warn — join yoksa alan düz FK'dır)
319
444
  - `set-user` adımı olmadan `user` okuyan middleware (MW-003, warn)
320
445
 
package/dist/server.js CHANGED
@@ -5,12 +5,14 @@ import { registerDeploymentTools } from './tools/deployments.js';
5
5
  import { registerDatasourceTools } from './tools/datasources.js';
6
6
  import { registerAutomationTools } from './tools/automations.js';
7
7
  import { registerOrganizationTools } from './tools/organizations.js';
8
+ import { registerExternalComponentTools } from './tools/externalComponents.js';
8
9
  import { registerComponentTools } from './tools/components.js';
9
10
  import { registerPlatformTools } from './tools/platformInfo.js';
10
11
  import { registerTemplateTools } from './tools/templates.js';
11
12
  import { registerWorkflowTools } from './tools/workflows.js';
12
13
  import { registerLayoutTools } from './tools/layouts.js';
13
14
  import { registerAccessTools } from './tools/access.js';
15
+ import { registerTranslationTools } from './tools/translations.js';
14
16
  import { registerFlowTools } from './tools/flows.js';
15
17
  import { registerKnowledgeResources } from './resources/knowledge.js';
16
18
  import { registerSchemaResources } from './resources/schemas.js';
@@ -41,12 +43,14 @@ const moduleRegistrations = {
41
43
  view: registerViewTools,
42
44
  page: registerPageTools,
43
45
  component: registerComponentTools,
46
+ externalComponent: registerExternalComponentTools,
44
47
  flow: registerFlowTools,
45
48
  layout: registerLayoutTools,
46
49
  variable: registerVariableTools,
47
50
  folder: registerFolderTools,
48
51
  file: registerFileTools,
49
52
  access: registerAccessTools,
53
+ translation: registerTranslationTools,
50
54
  template: registerTemplateTools,
51
55
  organization: registerOrganizationTools,
52
56
  dependency: registerDependencyTools,
@@ -0,0 +1,285 @@
1
+ import { guid, projectParam, environmentParam, readOnly, writes, updates, destructive } from './common.js';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { PlatformApiError } from '../platform/errors.js';
4
+ import { handle } from '../platform/responses.js';
5
+ import { basename, join, resolve } from 'node:path';
6
+ import { z } from 'zod';
7
+ /** Platformun köprü protokolü; scaffold ve publish bu sürüme göre üretir/doğrular */
8
+ const PROTOCOL_VERSION = 1;
9
+ /** Backend `ExternalComponentRules.MaxBundleSizeBytes` ile aynı sınır */
10
+ const maxBundleBytes = 2 * 1024 * 1024;
11
+ const slugPattern = /^x-[a-z][a-z0-9-]*$/;
12
+ const slugParam = z.string().regex(slugPattern)
13
+ .describe("Harici komponent slug'ı; 'x-' ile başlamak zorunda (örn. 'x-rating-stars') — bu önek platform slug'larıyla çakışmayı imkânsız kılar");
14
+ const versionParam = z.string().min(1).describe("Paket sürümü, örn. '1.0.0'; aynı sürüm ikinci kez yayınlanamaz");
15
+ /**
16
+ * Doğrulama raporunu ajanın düzeltme döngüsü kurabileceği şekilde özetler: block
17
+ * bulguları işi durdurur, warn bulguları aktivasyonda açık onay ister.
18
+ */
19
+ function summarizeFindings(findings) {
20
+ const blocking = (findings ?? []).filter(finding => finding.severity === 'block');
21
+ const warnings = (findings ?? []).filter(finding => finding.severity === 'warn');
22
+ return {
23
+ blocking,
24
+ warnings,
25
+ nextStep: blocking.length
26
+ ? 'Paket reddedildi ve depolamaya yazılmadı. Yukarıdaki block bulgularını kaynakta düzeltip yeniden yayınlayın.'
27
+ : warnings.length
28
+ ? "Paket taslak olarak kaydedildi. Uyarıları inceleyin; aktive etmek için external_component_activate'i acknowledgeWarnings:true ile çağırın."
29
+ : "Paket taslak olarak kaydedildi. Önizlemede doğruladıktan sonra external_component_activate ile yayına alın."
30
+ };
31
+ }
32
+ const elementTemplate = (slug, props) => `const PROPS = ${JSON.stringify(props)}
33
+
34
+ export default class ZeyrekElement extends HTMLElement {
35
+ #props = {}
36
+ #context = null
37
+ #machine = null
38
+
39
+ ${props.map(prop => ` set ${prop}(value) {
40
+ this.#props.${prop} = value
41
+ this.#render()
42
+ }`).join('\n\n')}
43
+
44
+ set zxContext(value) {
45
+ this.#context = value
46
+ this.#render()
47
+ }
48
+
49
+ set zxMachine(value) {
50
+ this.#machine = value
51
+ }
52
+
53
+ connectedCallback() {
54
+ this.#render()
55
+ }
56
+
57
+ /**
58
+ * Platforma bir olay bildirir. setter:true, data içindeki değerin aynı adlı
59
+ * prop'a geri yazılmasını ister — value gibi iki yönlü davranan proplar için.
60
+ */
61
+ emit(name, data, options = {}) {
62
+ this.dispatchEvent(new CustomEvent('zx-emit', { detail: { name, data, ...options } }))
63
+ }
64
+
65
+ #render() {
66
+ if (this.#context?.editorMode) {
67
+ this.textContent = '${slug}'
68
+ return
69
+ }
70
+
71
+ this.textContent = PROPS.map(prop => \`\${prop}: \${this.#props[prop] ?? ''}\`).join(' · ')
72
+ }
73
+ }
74
+ `;
75
+ const manifestTemplate = (slug, name, props) => JSON.stringify({
76
+ slug,
77
+ name,
78
+ description: `${name} — organizasyon tarafından yayınlanan komponent.`,
79
+ icon: 'widgets',
80
+ categories: ['common'],
81
+ protocolVersion: PROTOCOL_VERSION,
82
+ definition: {
83
+ props: props.map(prop => ({ title: prop, name: prop, type: 'text' })),
84
+ events: [{ name: 'change', title: 'When value changed' }],
85
+ validations: [],
86
+ blocks: [{ breakpoint: 1024, size: { w: 3, h: 8 }, minSize: { w: 1, h: 1 }, maxSize: { w: 12, h: 5 } }],
87
+ defaults: {}
88
+ }
89
+ }, null, 2);
90
+ const readmeTemplate = (slug) => `# ${slug}
91
+
92
+ Zeyrek harici komponenti. Köprü protokolü sürümü: ${PROTOCOL_VERSION}.
93
+
94
+ ## Sözleşme
95
+
96
+ Host, element'e şema prop'larını **JS property** olarak yazar (attribute değil —
97
+ dizi ve nesneler bozulmadan geçsin diye), yanına iki kanal koyar:
98
+
99
+ - \`zxContext\`: \`{ elementId, appearance, states, isRequired, editorMode, language }\`
100
+ - \`zxMachine\`: platform yüzeyi (state, değişken, dil, yetki, sibling prop'lar)
101
+
102
+ Element geriye tek bir DOM olayıyla konuşur: \`zx-emit\`, detail'i
103
+ \`{ name, data?, setter?, flowId?, params? }\`. \`setter: true\` verirseniz
104
+ \`data\` içindeki değer aynı adlı prop'a geri yazılır.
105
+
106
+ ## Kurallar
107
+
108
+ - Tek \`export default\`, \`HTMLElement\` türeten bir sınıf olmalı.
109
+ - \`eval\`, \`new Function\`, dinamik \`import()\`, \`document.cookie\`,
110
+ \`window.machine\`, service worker kaydı **yasak** — yayınlama reddedilir.
111
+ - Ağ erişimi, doğrudan storage ve \`innerHTML\` ataması uyarı üretir; aktive
112
+ ederken açık onay ister.
113
+ - Renk, boşluk ve tipografi için \`--ng-*\` tema değişkenlerini kullanın; sabit
114
+ renk yazmayın.
115
+
116
+ ## Yayınlama
117
+
118
+ 1. \`manifest.json\` içindeki prop ve event tanımlarını gerçek komponentinize göre düzenleyin.
119
+ 2. \`external_component_publish\` — kaydı ve paketi yükler, doğrulama raporunu döner.
120
+ 3. \`external_component_activate\` — taslağı yayına alır.
121
+ `;
122
+ export function registerExternalComponentTools(server, platform) {
123
+ const { config } = platform;
124
+ server.registerTool('external_component_list', {
125
+ title: 'Yayınlanan komponentleri listele',
126
+ description: "Organizasyonun kendi yayınladığı komponentleri (Studio'da 'Your Components' başlığı altında görünenler) listeler: slug, sürüm, taslak mı yayında mı. Platformun hazır komponentleri için component_list kullanın.",
127
+ inputSchema: {
128
+ project: projectParam,
129
+ environment: environmentParam,
130
+ applicationId: guid
131
+ },
132
+ annotations: readOnly
133
+ }, handle(config, async ({ project, environment, applicationId }) => {
134
+ const response = await platform.tenant('GET', project, `/components/${applicationId}`, {
135
+ environment,
136
+ query: { pageIndex: 0, pageLength: 999 }
137
+ });
138
+ const components = (response?.data ?? []).filter(component => component.origin === 'external');
139
+ return {
140
+ components: components.map(component => ({
141
+ id: component.id,
142
+ slug: component.slug,
143
+ name: component.name,
144
+ bundleVersion: component.bundleVersion ?? null,
145
+ bundleStatus: component.bundleStatus ?? null,
146
+ protocolVersion: component.protocolVersion ?? null
147
+ })),
148
+ note: components.length
149
+ ? undefined
150
+ : "Bu uygulamada yayınlanmış komponent yok. İhtiyacınızı karşılayan hazır bir komponent bulamadıysanız external_component_scaffold ile kendinizinkini yazmaya başlayabilirsiniz."
151
+ };
152
+ }));
153
+ server.registerTool('external_component_scaffold', {
154
+ title: 'Harici komponent iskeleti üret',
155
+ description: "Yerel bir dizine çalışan bir harici komponent iskeleti yazar: element.mjs (köprü protokolünü karşılayan sınıf), manifest.json (prop/event/boyut tanımı) ve README. Mevcut katalog ihtiyacınızı karşılamıyorsa buradan başlayın; ürettiği dosyaları düzenleyip external_component_publish ile yayınlarsınız. Dosyalar gateway sürecinin çalıştığı makineye yazılır, içerikleri model bağlamına girmez.",
156
+ inputSchema: {
157
+ directory: z.string().min(1).describe('İskeletin yazılacağı yerel dizin; yoksa oluşturulur'),
158
+ slug: slugParam,
159
+ name: z.string().min(1).describe("Studio toolbox'ta görünecek ad"),
160
+ props: z.array(z.string().min(1)).optional().describe("Komponentin alacağı prop adları; verilmezse ['value'] varsayılır. id/class/style/name gibi DOM adları kullanılamaz")
161
+ },
162
+ annotations: writes
163
+ }, handle(config, async ({ directory, slug, name, props }) => {
164
+ const propNames = props?.length ? props : ['value'];
165
+ const reserved = propNames.filter(prop => /^(id|class|className|style|slot|name|children|innerHTML|outerHTML|textContent)$/.test(prop) || prop.startsWith('zx') || prop.startsWith('on'));
166
+ if (reserved.length) {
167
+ throw new PlatformApiError(`external_component_scaffold: şu prop adları platform tarafından rezerve edilmiş ve yayınlama sırasında reddedilir: ${reserved.join(', ')}.`);
168
+ }
169
+ const target = resolve(directory);
170
+ await mkdir(target, { recursive: true });
171
+ const files = [
172
+ { path: join(target, 'element.mjs'), content: elementTemplate(slug, propNames) },
173
+ { path: join(target, 'manifest.json'), content: manifestTemplate(slug, name, propNames) },
174
+ { path: join(target, 'README.md'), content: readmeTemplate(slug) }
175
+ ];
176
+ for (const file of files) {
177
+ await writeFile(file.path, file.content, 'utf8');
178
+ }
179
+ return {
180
+ directory: target,
181
+ files: files.map(file => basename(file.path)),
182
+ protocolVersion: PROTOCOL_VERSION,
183
+ nextStep: `element.mjs ve manifest.json dosyalarını komponentinize göre düzenleyin, sonra external_component_publish ile bundlePath='${join(target, 'element.mjs')}' ve manifestPath='${join(target, 'manifest.json')}' vererek yayınlayın.`
184
+ };
185
+ }));
186
+ server.registerTool('external_component_publish', {
187
+ title: 'Harici komponenti yayınla',
188
+ description: "Komponent kaydını ve paketini platforma yükler. Paket sunucudaki doğrulama hattından geçer (şema + güvenlik taraması); block bulgusu varsa depolamaya hiç yazılmaz ve rapor döner. Kabul edilen paket taslak (draft) olarak kaydedilir — Studio önizlemesinde görünür, yayınlanmış uygulamada görünmez; yayına almak için external_component_activate. Dosyalar gateway sürecinin okuyabildiği yerel yollardır, içerikleri model bağlamına girmez.",
189
+ inputSchema: {
190
+ project: projectParam,
191
+ environment: environmentParam,
192
+ applicationId: guid,
193
+ bundlePath: z.string().min(1).describe('Tek dosyalık ES module paketinin yerel yolu (.mjs)'),
194
+ manifestPath: z.string().min(1).describe('manifest.json yerel yolu; slug, name, definition ve protocolVersion buradan okunur'),
195
+ version: versionParam
196
+ },
197
+ annotations: writes
198
+ }, handle(config, async ({ project, environment, applicationId, bundlePath, manifestPath, version }) => {
199
+ const manifestFile = resolve(manifestPath);
200
+ const bundleFile = resolve(bundlePath);
201
+ let manifest;
202
+ try {
203
+ manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
204
+ }
205
+ catch (error) {
206
+ throw new PlatformApiError(`external_component_publish: manifest okunamadı ya da geçerli JSON değil — ${manifestFile} (${error instanceof Error ? error.message : String(error)}).`);
207
+ }
208
+ const slug = typeof manifest.slug === 'string' ? manifest.slug : undefined;
209
+ if (!slug || !slugPattern.test(slug)) {
210
+ throw new PlatformApiError(`external_component_publish: manifest'teki slug 'x-' ile başlamalı ve küçük harf/rakam/tire içermeli — okunan: ${String(manifest.slug)}.`);
211
+ }
212
+ let bytes;
213
+ try {
214
+ bytes = await readFile(bundleFile);
215
+ }
216
+ catch (error) {
217
+ throw new PlatformApiError(`external_component_publish: paket okunamadı — ${bundleFile} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`);
218
+ }
219
+ if (bytes.byteLength > maxBundleBytes) {
220
+ throw new PlatformApiError(`external_component_publish: paket ${maxBundleBytes} bayt sınırını aşıyor (${bytes.byteLength} B).`);
221
+ }
222
+ const record = await platform.tenant('POST', project, `/components/${applicationId}/external`, {
223
+ environment,
224
+ body: {
225
+ slug,
226
+ name: manifest.name,
227
+ description: manifest.description,
228
+ icon: manifest.icon,
229
+ categories: manifest.categories ?? [],
230
+ definition: manifest.definition,
231
+ protocolVersion: manifest.protocolVersion ?? PROTOCOL_VERSION
232
+ }
233
+ });
234
+ const bundle = await platform.tenant('POST', project, `/components/${applicationId}/external/${slug}/bundle`, {
235
+ environment,
236
+ multipart: [
237
+ { name: 'Version', value: version },
238
+ { name: 'Bundle', filename: basename(bundleFile), contentType: 'text/javascript', content: bytes }
239
+ ]
240
+ });
241
+ return {
242
+ component: record,
243
+ bundle: bundle?.data,
244
+ ...summarizeFindings(bundle?.data?.findings)
245
+ };
246
+ }));
247
+ server.registerTool('external_component_activate', {
248
+ title: 'Paket sürümünü yayına al',
249
+ description: "Taslak bir paket sürümünü yayına alır; yalnızca metadata değişir, platform yeniden başlatılmaz ve açık oturumlar bozulmaz. Eski bir sürüm verilirse aynı mekanizma geri alma (rollback) olarak çalışır. Doğrulama uyarısı olan bir sürüm acknowledgeWarnings:true olmadan aktive edilemez.",
250
+ inputSchema: {
251
+ project: projectParam,
252
+ environment: environmentParam,
253
+ applicationId: guid,
254
+ slug: slugParam,
255
+ version: versionParam,
256
+ acknowledgeWarnings: z.boolean().optional().describe('Doğrulama uyarılarını okuyup kabul ettiğinizi bildirir; uyarısı olan sürümlerde zorunlu')
257
+ },
258
+ annotations: updates
259
+ }, handle(config, ({ project, environment, applicationId, slug, version, acknowledgeWarnings }) => platform.tenant('POST', project, `/components/${applicationId}/external/${slug}/activate`, {
260
+ environment,
261
+ body: { version, acknowledgeWarnings: acknowledgeWarnings === true }
262
+ })));
263
+ server.registerTool('external_component_versions', {
264
+ title: 'Paket sürümlerini listele',
265
+ description: 'Bir harici komponentin yayınlanmış paket sürümlerini, hangisinin aktif olduğunu ve her sürümün doğrulama raporunu listeler. Geri alınacak sürümü buradan seçin.',
266
+ inputSchema: {
267
+ project: projectParam,
268
+ environment: environmentParam,
269
+ applicationId: guid,
270
+ slug: slugParam
271
+ },
272
+ annotations: readOnly
273
+ }, handle(config, ({ project, environment, applicationId, slug }) => platform.tenant('GET', project, `/components/${applicationId}/external/${slug}/versions`, { environment })));
274
+ server.registerTool('external_component_delete', {
275
+ title: 'Harici komponenti sil',
276
+ description: 'Yayınlanan komponent kaydını siler. Sayfalarda kullanılıyorsa o sayfalar komponenti kaybeder ve yerinde bir hata kartı görünür — silmeden önce dependency_graph ile kullanımını kontrol edin.',
277
+ inputSchema: {
278
+ project: projectParam,
279
+ environment: environmentParam,
280
+ applicationId: guid,
281
+ id: guid.describe('external_component_list çıktısındaki komponent id')
282
+ },
283
+ annotations: destructive
284
+ }, handle(config, ({ project, environment, applicationId, id }) => platform.tenant('DELETE', project, `/components/${applicationId}/${id}`, { environment })));
285
+ }
@@ -1,17 +1,30 @@
1
1
  import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
2
2
  import { normalizeComponentDefaults, componentDefaultsNote, blockSizeNote, heightTypeNote, invalidScreenEventNames, invalidWidgetTypeNodes, verticalGapWarnings, withNormalization } from '../knowledge/normalize.js';
3
- import { fetchHost, layoutUpdateBody, schemaParam, eventsParam } from './screens.js';
3
+ import { fetchHost, layoutUpdateBody, schemaParam, eventsParam, modeParam, summarize, findNode } from './screens.js';
4
4
  import { applyCatalogLayoutRules } from './components.js';
5
5
  import { PlatformApiError } from '../platform/errors.js';
6
6
  import { handle } from '../platform/responses.js';
7
7
  import { z } from 'zod';
8
8
  const viewsParam = schemaParam.describe("Layout'un komponent ağacı. DİKKAT: layout'ta şema alanının adı 'schema' değil 'views'tir ve sayfa micro-op hattı (page_schema_patch) layout'a uygulanmaz. İçinde MUTLAKA bir { type: 'slot' } komponenti olmalı — sayfa içeriği YALNIZ slot'un içine render edilir, slot'suz layout'a bağlanan sayfalar boş görünür.");
9
+ function countSlots(nodes) {
10
+ if (!Array.isArray(nodes))
11
+ return 0;
12
+ return nodes.reduce((total, node) => {
13
+ if (!node || typeof node !== 'object')
14
+ return total;
15
+ const record = node;
16
+ return total + (record.type === 'slot' ? 1 : 0) + countSlots(record.children);
17
+ }, 0);
18
+ }
9
19
  function slotWarning(views) {
10
- const hasSlot = (nodes) => Array.isArray(nodes) && nodes.some(node => Boolean(node) && typeof node === 'object' &&
11
- (node.type === 'slot' || hasSlot(node.children)));
12
- return hasSlot(views)
13
- ? []
14
- : ["LAY-002: layout'ta 'slot' tipli komponent YOK — Studio ve runtime, sayfa şemasını yalnız layout'taki slot'un içine yerleştirir (page.mappers: layout varsa render şeması layout views'üdür); bu layout'a bağlanan her sayfanın içeriği görünmez olur. views'e bir { type: 'slot' } komponenti ekleyin ve page_state.layoutSlots ile doğrulayın."];
20
+ const slots = countSlots(views);
21
+ if (slots === 0) {
22
+ return ["LAY-002: layout'ta 'slot' tipli komponent YOK — Studio ve runtime, sayfa şemasını yalnız layout'taki slot'un içine yerleştirir (page.mappers: layout varsa render şeması layout views'üdür); bu layout'a bağlanan her sayfanın içeriği görünmez olur. views'e bir { type: 'slot' } komponenti ekleyin ve page_state.layoutSlots ile doğrulayın."];
23
+ }
24
+ if (slots > 1) {
25
+ return [`LAY-006: layout'ta ${slots} adet 'slot' var — enjeksiyon HER slot'a AYRI AYRI kopyalar (layout.functions.ts injectPageSchemaIntoSlots ağacı dolaşır ve type==='slot' gören her düğümün children'ına sayfa şemasının klonunu yazar; önizlemede page.mappers.mapPageLayoutToPreview aynısını yapar). Sonuç: sayfa birden çok kez çizilir ve aynı elementId'ler tekrarlanır. Tek slot bırakın.`];
26
+ }
27
+ return [];
15
28
  }
16
29
  export function registerLayoutTools(server, platform) {
17
30
  const config = platform.config;
@@ -27,6 +40,35 @@ export function registerLayoutTools(server, platform) {
27
40
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, id: guid },
28
41
  annotations: readOnly
29
42
  }, handle(config, ({ project, environment, applicationId, id }) => platform.tenant('GET', project, `/layouts/${applicationId}/${id}`, { environment })));
43
+ server.registerTool('layout_schema_get', {
44
+ title: "Layout komponent ağacını oku",
45
+ description: "Layout'un komponent ağacını (views) okur — page_schema_get'in layout karşılığı. DİKKAT: layout'ta micro-op hattı YOKTUR (backend'de PATCH /layouts/.../schema ucu yok), yani buradan okunan updatedAt bir expectedUpdatedAt kontrolünde kullanılamaz; değişiklik layout_update ile tam gövde olarak yazılır.",
46
+ inputSchema: {
47
+ project: projectParam,
48
+ environment: environmentParam,
49
+ applicationId: guid,
50
+ id: guid,
51
+ mode: modeParam,
52
+ componentId: z.string().optional().describe("subtree kipinde kök alınacak komponentin elementId'si")
53
+ },
54
+ annotations: readOnly
55
+ }, handle(config, async ({ project, environment, applicationId, id, mode, componentId }) => {
56
+ const layout = await fetchHost(platform, 'layout', project, environment, applicationId, id);
57
+ const audit = layout.audit;
58
+ const updatedAt = audit?.updatedAt ?? audit?.createdAt;
59
+ const views = (layout.views ?? []);
60
+ if (mode === 'full')
61
+ return { updatedAt, views };
62
+ if (mode === 'subtree') {
63
+ if (!componentId)
64
+ throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
65
+ const subtree = findNode(views, componentId);
66
+ if (!subtree)
67
+ throw new PlatformApiError(`Layout'ta '${componentId}' id'li bir komponent yok; layout_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
68
+ return { updatedAt, views: subtree };
69
+ }
70
+ return { updatedAt, views: views.map(node => summarize(node)) };
71
+ }));
30
72
  server.registerTool('layout_create', {
31
73
  title: 'Layout oluştur',
32
74
  description: "Yeni layout oluşturur. Şablon için mevcut bir layout'u layout_get ile alın; slot kurgusunu page_state çıktısındaki layoutSlots ile doğrulayın.",