@appaflytech/zeyrek-ai-mcp 0.0.20 → 0.0.22
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 +937 -212
- package/dist/knowledge/componentEvents.js +366 -66
- package/dist/knowledge/mobileComponentCatalog.js +213 -1
- package/dist/knowledge/mobileComponentEvents.js +48 -0
- package/dist/knowledge/normalize.js +55 -11
- package/dist/knowledge/platform/mobileRules.js +7 -5
- 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/component.md +9 -1
- package/dist/resources/guides/functions.md +31 -2
- package/dist/resources/guides/getting-started.md +35 -0
- package/dist/resources/guides/mobile.md +51 -2
- package/dist/resources/guides/page.md +34 -16
- package/dist/resources/guides/translation.md +5 -6
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +18 -0
- package/dist/tools/designSchemaPatch.js +9 -5
- package/dist/tools/flows.js +36 -42
- package/dist/tools/functions.js +20 -10
- package/dist/tools/microservices.js +39 -11
- package/dist/tools/mobile.js +1 -1
- package/dist/tools/pages.js +65 -17
- package/dist/tools/schemaOps.js +2 -2
- package/dist/tools/workflows.js +39 -12
- package/package.json +7 -2
|
@@ -97,8 +97,9 @@ mümkündür ama doğrulama duvarı ve canlı canvas yalnız `page_schema_patch`
|
|
|
97
97
|
komponentin iç kimliğidir (aşağıda Kural 2, `FLOW-018`).
|
|
98
98
|
|
|
99
99
|
- **`blocks[].size`'ı komponentin `component_get`'teki `minSize`..`maxSize` aralığında tut.** Dikkat:
|
|
100
|
-
katalog varsayılanı aşabiliyor (`
|
|
101
|
-
|
|
100
|
+
katalog varsayılanı aşabiliyor (`datepicker` default `h:8`, `maxSize` `h:5`; 18 şemada aynı
|
|
101
|
+
durum, liste zeyrek-frontend `docs/issues.md` ARC-013 — `page-header` 2026-09-18'de sınırı
|
|
102
|
+
12'ye açarak çıktı) — varsayılanı körü körüne kopyalama. Sınır grid kanvasının kuralıdır; API 2026-09-15'ten beri reddetmez, ama aşan
|
|
102
103
|
blok kanvasta boyutlandırılamaz (aşağıda Kural 7 "Boyut tuzağı").
|
|
103
104
|
- **Kurulmuş bir düğümün boyutunu `resize_component` micro-op'u değiştirir.**
|
|
104
105
|
`{ op: 'resize_component', target: '<elementId>', size: { w, h } }` ızgara boyutunu,
|
|
@@ -163,19 +164,32 @@ Sayfa açılışında veri çekmek için `mount` kullanılır ama tek seçenek o
|
|
|
163
164
|
|
|
164
165
|
Yani "satırı seçince detay getir" (`select`), "kolondan süzünce sunucudan yeniden çek"
|
|
165
166
|
(`filter`), "sayfa değişince yeni sayfayı çek" (`pagination`), "satıra çift tıklayınca formu aç"
|
|
166
|
-
(`doubleClick`) akışları da aynı `set_event` mekanizmasıyla kurulur. Buna karşılık `
|
|
167
|
-
|
|
167
|
+
(`doubleClick`) akışları da aynı `set_event` mekanizmasıyla kurulur. Buna karşılık `datatable`
|
|
168
|
+
yalnız `mount` yayar (bilinçli — Kural 1e); `page-header` 2026-09-18'den beri izdeki öğe
|
|
169
|
+
tıklamasını `clickItem` olarak yayar (`component_get` yükü verir).
|
|
168
170
|
|
|
169
171
|
**Olay adları `on` ÖNEKİ ALMAZ** (`button` → `click`, `form` → `submit`), tek istisna katalogda
|
|
170
172
|
`texteditor`'dur (`onMount`/`onChange`). **Komponentin desteklemediği bir olay adı verirsen bağ
|
|
171
173
|
sessizce ölü kalır** — önce `component_get`. Gateway `set_event`'i katalogla karşılaştırır ve
|
|
172
174
|
uymayan adı **PAGE-008** ile istek gitmeden reddeder.
|
|
173
175
|
|
|
176
|
+
**Bir olaya birden fazla akış bağlanabilir — ikinciyi `add_event` ile ekle.** `set_event` olayın
|
|
177
|
+
BÜTÜN bağlarını tek akışa indirir: başlık alanının `change` olayına makale önerisi bağlıyken akıllı
|
|
178
|
+
öneriyi `set_event` ile bağlamak makale önerisini devre dışı bırakır. Gateway bunu **PAGE-034** ile
|
|
179
|
+
istek gitmeden reddeder. Doğru op `{ "op": "add_event", "target": "<elementId>", "event": "change",
|
|
180
|
+
"eventValue": "<ikinci flow id>" }`'dir. Olay tetiklenince bağlı akışların hepsi **paralel ve
|
|
181
|
+
birbirinden bağımsız** koşar: biri hata verirse diğeri etkilenmez, **sıra garantisi yoktur**. Biri
|
|
182
|
+
diğerinin sonucunu bekliyorsa ya da ikisi aynı değişkene/prop'a yazıyorsa ayrı bağ değil,
|
|
183
|
+
`flow-trigger` adımıyla zincir kur (Kural 1c). Mevcut bağı bilerek değiştirmek için `set_event`
|
|
184
|
+
op'una `replaceBoundFlows: true` ekle. **Mobil runtime yalnız ilk akışı koşar** — mobil
|
|
185
|
+
uygulamada olay başına tek akış bağla.
|
|
186
|
+
|
|
174
187
|
**Olay KALDIRMAK için `remove_event` kullan.** Platformun `set_event` micro-op'u olay silemez:
|
|
175
188
|
`eventValue: null` verildiğinde kayıt `{ name: 'click' }` olarak (değersiz) kalır, diziden
|
|
176
189
|
çıkmaz — şemada ölü bağ artığı bırakır. `{ "op": "remove_event", "target": "<elementId>",
|
|
177
190
|
"event": "click" }` platformun op'udur: olayı diziden çıkarır (yanıtta `removedEvents`); olmayan
|
|
178
|
-
bir olayı kaldırmaya çalışmak `missing-event` ile 422 döner.
|
|
191
|
+
bir olayı kaldırmaya çalışmak `missing-event` ile 422 döner. Olaya birden fazla akış bağlıysa
|
|
192
|
+
kaldırılacak akışı `eventValue` ile ver; vermezsen `ambiguous-event` ile 422 döner. Sayfa düzeyi olaylar için
|
|
179
193
|
`target: "page"`. Referanssız kalan akış silineceği için `allowFlowDeletion` kuralı (G-21) bu op'ta
|
|
180
194
|
da geçerlidir.
|
|
181
195
|
|
|
@@ -189,12 +203,14 @@ olaylarında **boştur** (ölçüm 2026-08-25, kaynak okuması: `flow-machine.ts
|
|
|
189
203
|
Pratik kural: **`payload` anahtarları aynı zamanda prop adlarıdır; akışta `self.<anahtar>` ile
|
|
190
204
|
oku** (ya da `value { type:'property', ref:{ source:'component', element, property } }` ile).
|
|
191
205
|
`event.data` yalnız `params` ile gelen olaylarda dolar — `html` komponentinin `fireEvent`
|
|
192
|
-
aksiyonları (`platform_guide('html')`) ve
|
|
193
|
-
`importwizard` · `kanban` · `signature` · `stepper`
|
|
194
|
-
|
|
195
|
-
yazmaz (`component_get` çıktısındaki sözleşme notları ayrımı
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
aksiyonları (`platform_guide('html')`) ve **`params`'ı da gönderen komponentler**
|
|
207
|
+
(`eventcalendar` · `importwizard` · `kanban` · `signature` · `stepper` · `tabs` ·
|
|
208
|
+
`tree-view` · `pagination`): bunların yükü `event.data.<anahtar>` ile de okunur; `kanban` ve
|
|
209
|
+
`eventcalendar` yükü `self`'e HİÇ yazmaz (`component_get` çıktısındaki sözleşme notları ayrımı
|
|
210
|
+
söyler). Sessiz hata örneği: **tablonun kendi** sayfalama akışını (`table.pagination`)
|
|
211
|
+
`event.data.limit`'e bağlarsan sorgu `LIMIT null OFFSET null` ile koşar ve **tüm satırları**
|
|
212
|
+
döndürür; hata yok, sayfalama yok. (Bağımsız `pagination` komponenti bu tuzağa girmez, yükünü
|
|
213
|
+
`params` ile de gönderir.)
|
|
198
214
|
|
|
199
215
|
| Komponent · olay | Yük (aynı zamanda prop adları) | Akışta nereden okunur |
|
|
200
216
|
|---|---|---|
|
|
@@ -204,16 +220,18 @@ sayfalama akışını `event.data.limit`'e bağlarsan sorgu `LIMIT null OFFSET n
|
|
|
204
220
|
| `table.select` · `doubleClick` | — | `self.selectedRow` |
|
|
205
221
|
| `table.selectionChange` | — | `self.selectedRows` |
|
|
206
222
|
| `datatable.mount` | — | `self.filters` · `self.sortField` · `self.sortType` · `self.offset` |
|
|
207
|
-
| `pagination.change` | `{ offset, limit }` | `self.offset` · `self.limit` |
|
|
223
|
+
| `pagination.change` | `{ offset, limit }` | `self.offset` · `self.limit` ya da `event.data.offset` · `event.data.limit` |
|
|
208
224
|
| `textinput/textarea/password.change` | `{ value }` | `self.value` |
|
|
209
225
|
| `dropdown.change` · `multi-select.change` | `{ value }` | `self.value` |
|
|
210
226
|
| `dropdown.search` · `multi-select.search` | `{ searchText }` | `self.searchText` |
|
|
211
|
-
| `tabs.tab` | `{ selected }` | `self.selected` |
|
|
227
|
+
| `tabs.tab` | `{ selected }` | `self.selected` ya da `event.data.selected` |
|
|
212
228
|
| `chart.dataPointClick` | `{ name, seriesName, value, dataIndex, row }` | `self.row` (kaynak satırın tamamı) |
|
|
213
|
-
| `repeater.change` | `{ selectedIndex }` | `
|
|
229
|
+
| `repeater.change` · `repeater.select` | `{ selectedIndex }` | `self.selectedIndex` · `self.selectedItem` (akış satır bağlamında koşmaz, `item`/`Row` boş) |
|
|
214
230
|
| `html` aksiyonu (`fireEvent`) | ikinci argüman | **`event.data`** |
|
|
215
231
|
| `kanban.cardMove` | `{ id, row, from, to }` | **`event.data`** (self'e yazılmaz) |
|
|
232
|
+
| `kanban.cardAdd` · `kanban.cardRemove` | `{ column }` · `{ id, row, column }` | **`event.data`** (self'e yazılmaz; kart eklenmez/gizlenmez, akış yazıp `rows`'u tazeler) |
|
|
216
233
|
| `eventcalendar.eventMove` | `{ id, row, start, end, previousStart, previousEnd }` | **`event.data`** (self'e yazılmaz) |
|
|
234
|
+
| `eventcalendar.monthChange` · `eventcalendar.viewChange` · `eventcalendar.eventCreate` | `{ month, year, view, start, end }` · `{ view, start, end }` · `{ start, end, allDay }` | **`event.data`** (self'e yazılmaz; veri sorgusunu kesişimle bağla: başlangıç < `end`+1 gün ve bitiş >= `start`) |
|
|
217
235
|
| `importwizard.complete` | `{ rows, errors, fileName, total }` | `self.rows`… ya da `event.data` |
|
|
218
236
|
| `stepper.stepChange` | `{ currentStep, previous }` | `self.currentStep` ya da `event.data` |
|
|
219
237
|
| `signature.change` | `{ value }` | `self.value` ya da `event.data.value` |
|
|
@@ -1338,7 +1356,7 @@ budur — bir preset ya da `componentStyles` sizin değerinizi eziyorsa `styleLa
|
|
|
1338
1356
|
| Değişiklik | Tool | Neden |
|
|
1339
1357
|
|---|---|---|
|
|
1340
1358
|
| Komponent ekle/sil/taşı, prop, binding, event | `page_schema_patch` | Doğrulama duvarı + açık Studio canvas'ına SignalR canlı yayını + eşzamanlı yazar kilidi yalnız bu yolda çalışır |
|
|
1341
|
-
| Ad, slug, browserTitle, `browserTitleKey`, layout, roller, middleware | `page_update` | Üstbilgi alanları micro-op sözlüğünde yok. `browserTitleKey` sekme başlığını çeviri anahtarına bağlar: verilirse başlık sözlükten çözülür, boşsa `browserTitle` kullanılır. Başlık
|
|
1359
|
+
| Ad, slug, browserTitle, `browserTitleKey`, layout, roller, middleware | `page_update` | Üstbilgi alanları micro-op sözlüğünde yok. `browserTitleKey` sekme başlığını çeviri anahtarına bağlar: verilirse başlık sözlükten çözülür, boşsa `browserTitle` kullanılır. Başlık canlı dil değişimini izler; anahtarın o dilde değeri yoksa varsayılan dile düşer (`platform_guide('translation')`) |
|
|
1342
1360
|
|
|
1343
1361
|
Platformun `PUT /pages/...` ucu **replace**'tir: gönderilmeyen `schema`/`events`/`roleIds`
|
|
1344
1362
|
alanları silinir. Gateway bu yüzden `page_update`'te sayfayı önce okur, yalnız verdiğin
|
|
@@ -1853,7 +1871,7 @@ bağlanır, filtre `source:'expression'` + `value:'user.id'` olur.
|
|
|
1853
1871
|
| Kimlik alanları | `email` · `password` |
|
|
1854
1872
|
| Dosya | `fileUpload` (dropzone) · `fileInput` · `filebutton` · `file-viewer` |
|
|
1855
1873
|
| Liste | `table` · `datatable` (Kural 1e) · `repeater` (kart listesi VE dinamik form — Kural 1g) · `tree-view` (hiyerarşi; `isSearchable` ile istemci içi arama) · `timeline` (tarihli olay akışı) |
|
|
1856
|
-
| Pano / takvim | `kanban` (durum kolonlarında sürüklenebilir kartlar; taşıma `cardMove` ile `event.data`'dan okunur, kalıcılık AKIŞIN işidir) · `eventcalendar` (ay/hafta/gün; veri `rows`'a akışla yazılır, `monthChange`
|
|
1874
|
+
| Pano / takvim | `kanban` (durum kolonlarında sürüklenebilir kartlar; taşıma `cardMove` ile `event.data`'dan okunur, kalıcılık AKIŞIN işidir) · `eventcalendar` (ay/hafta/gün/ajanda; veri `rows`'a akışla yazılır, `monthChange`/`viewChange` görünen `start`/`end` aralığıyla tazeleme kancasıdır) |
|
|
1857
1875
|
| Kayıt detayı | `detailview` — tek kaydı etiket/değer ızgarası basar (alan başına format, iç içe alan yolu); OLAY YAYMAZ, tıklanabilirlik gerekiyorsa kapsayıcıya sar |
|
|
1858
1876
|
| Adımlı akış | `stepper` — tabs gibi her adım kendi droppable görünümünü taşır; ileri gidiş `canProceed` kapısıyla (`{{ form1.isValid === true }}` biçimi), adım `currentStep` setter'ıyla sürülür |
|
|
1859
1877
|
| İmza / CSV içe aktarma | `signature` (değer PNG data-URL ya da ham SVG; `change` setter'lı) · `importwizard` (3 adımlı sihirbaz; temiz+hatalı satırlar `complete` yükünde — satırları uca yazmak akışın işidir) |
|
|
@@ -175,11 +175,10 @@ motor olmasıdır (tarayıcı JS'i ve Jint).
|
|
|
175
175
|
verilmişse sekme başlığı çeviri sözlüğünden çözülür; anahtar boşsa `browserTitle` olduğu gibi
|
|
176
176
|
kullanılır. İkisi de boşsa sayfa adı yazılır.
|
|
177
177
|
|
|
178
|
-
> **Sekme başlığı
|
|
179
|
-
>
|
|
180
|
-
>
|
|
181
|
-
>
|
|
182
|
-
> çözülmesi. Sekme başlığının dilini doğrulayacaksan **yeniden yükleyerek** doğrula.
|
|
178
|
+
> **Sekme başlığı canlı dil değişimini İZLER.** `language-selector` ile dil değiştirildiğinde
|
|
179
|
+
> sayfa içeriğiyle birlikte sekme başlığı da yeni dilde çözülür; yeniden yükleme gerekmez
|
|
180
|
+
> (ölçüldü 2026-09-11). Anahtarın seçilen dilde değeri yoksa projenin varsayılan diline düşer.
|
|
181
|
+
> 2026-09-08 ölçümündeki "açılıştaki dilde kalır" davranışı düzeltildi.
|
|
183
182
|
|
|
184
183
|
## Doğrulama mesajları: `t:` öneki
|
|
185
184
|
|
|
@@ -209,7 +208,7 @@ gerekmez — `schema.ts`'e dokunulmaz, katalog yeniden yüklenmez.
|
|
|
209
208
|
`translation_update` artık uygulamanın bütün sayfalarının önbellek satırlarını düşürüyor
|
|
210
209
|
(`RefreshPageCacheWhenTranslationChangedCommand`), okuma yolu cache-aside olduğu için bir
|
|
211
210
|
sonraki `page_runtime_get` taze değeri kurar. Yazmadan hemen sonra okuyabilirsin.
|
|
212
|
-
- Mobil uygulama runtime'ı (`@ng-apps/ui-native
|
|
211
|
+
- Mobil uygulama runtime'ı (`@ng-apps/ui-native`, Expo) web ile aynı sözlüğü ve motoru kullanır (2026-09-25, plans/mobil-coklu-dil): `language-selector` mobil katalogda, cihazda dil sırası uygulama içi seçim → kayıtlı seçim → cihaz dili → varsayılan (`?lang` yalnız `/preview`'da), seçim uygulama çapında ve kalıcı, sekme etiketi `t:<anahtar>`, `X-Data-Language` sonraki isteklerle gider. Ayrıntı `platform_guide('mobile')` §Çok dil.
|
|
213
212
|
|
|
214
213
|
## Backend doğrulama sınırları (TR-001)
|
|
215
214
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
3
|
-
"generatedAt": "2026-09-
|
|
2
|
+
"hash": "f37265d598ea468deb2157165891629c2e88c7fd2238e82ec60cda7f58f7de4c",
|
|
3
|
+
"generatedAt": "2026-09-18T12:57:15.416Z",
|
|
4
4
|
"types": 186,
|
|
5
5
|
"files": 108,
|
|
6
6
|
"note": "Şemaları üreten backend kaynaklarının parmak izi. Elle düzenlemeyin: 'npm run generate-schemas' yazar, 'npm run schema:check' denetler.",
|
|
@@ -95,6 +95,20 @@
|
|
|
95
95
|
"$ref": "#/components/schemas/NgApps.Domain.Contexts.Shared.Dependencies.DependencyNode"
|
|
96
96
|
},
|
|
97
97
|
"nullable": true
|
|
98
|
+
},
|
|
99
|
+
"bindings": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"items": {
|
|
102
|
+
"$ref": "#/components/schemas/NgApps.Domain.Contexts.Shared.Dependencies.DependencyNode"
|
|
103
|
+
},
|
|
104
|
+
"nullable": true
|
|
105
|
+
},
|
|
106
|
+
"functions": {
|
|
107
|
+
"type": "array",
|
|
108
|
+
"items": {
|
|
109
|
+
"$ref": "#/components/schemas/NgApps.Domain.Contexts.Shared.Dependencies.DependencyNode"
|
|
110
|
+
},
|
|
111
|
+
"nullable": true
|
|
98
112
|
}
|
|
99
113
|
},
|
|
100
114
|
"additionalProperties": false
|
|
@@ -189,6 +203,10 @@
|
|
|
189
203
|
},
|
|
190
204
|
"stepKind": {
|
|
191
205
|
"$ref": "#/components/schemas/NgApps.Domain.Contexts.Studio.Flows.StepKind"
|
|
206
|
+
},
|
|
207
|
+
"propertyName": {
|
|
208
|
+
"type": "string",
|
|
209
|
+
"nullable": true
|
|
192
210
|
}
|
|
193
211
|
},
|
|
194
212
|
"additionalProperties": false
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, staleSetterWarnings, normalizeSchemaOps, validationNote, componentDefaultsNote, replacePropNote, flexSizeWarnings, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, verticalGapWarnings, withNormalization, FLEX_LAYOUT_PROP_NAMES } from '../knowledge/normalize.js';
|
|
1
|
+
import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEventOps, stripGatewayEventFields, staleSetterWarnings, normalizeSchemaOps, validationNote, componentDefaultsNote, replacePropNote, flexSizeWarnings, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, verticalGapWarnings, withNormalization, FLEX_LAYOUT_PROP_NAMES } from '../knowledge/normalize.js';
|
|
2
2
|
import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, removeNodeFromTree, planEventRemovals, planResizes, planPropTypes, assertOpBodies } from './schemaOps.js';
|
|
3
3
|
import { guid, projectParam, environmentParam, updates, microOps } from './common.js';
|
|
4
4
|
import { fetchHost, hostPath } from './screens.js';
|
|
5
5
|
import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
|
|
6
|
-
import { schemaOpDrops, withWriteDiff } from '../platform/writeDiff.js';
|
|
6
|
+
import { emptyWriteDiff, schemaOpDrops, withWriteDiff } from '../platform/writeDiff.js';
|
|
7
7
|
import { fetchComponentEventNames } from './components.js';
|
|
8
8
|
import { PlatformApiError } from '../platform/errors.js';
|
|
9
9
|
import { handle } from '../platform/responses.js';
|
|
@@ -70,7 +70,10 @@ export function registerDesignSchemaPatchTool(server, platform, host) {
|
|
|
70
70
|
}
|
|
71
71
|
const nodes = (record[spec.tree] ?? []);
|
|
72
72
|
const events = host === 'layout' ? record.events : undefined;
|
|
73
|
-
if (opRecords.some(entry => entry.op === 'set_event')) {
|
|
73
|
+
if (opRecords.some(entry => entry.op === 'set_event' || entry.op === 'add_event')) {
|
|
74
|
+
const bound = boundEventOps(nodes, opRecords);
|
|
75
|
+
if (bound.length > 0)
|
|
76
|
+
throw new PlatformApiError(`PAGE-034 — dolu olaya set_event (istek gönderilmedi): ${bound.join(' · ')}`, 400);
|
|
74
77
|
const typesByElementId = new Map();
|
|
75
78
|
collectTypes(nodes, typesByElementId);
|
|
76
79
|
collectTypes(opRecords.map(entry => entry.component).filter(Boolean), typesByElementId);
|
|
@@ -79,6 +82,7 @@ export function registerDesignSchemaPatchTool(server, platform, host) {
|
|
|
79
82
|
if (unknown.length > 0)
|
|
80
83
|
throw new PlatformApiError(`PAGE-008 — geçersiz olay adı (istek gönderilmedi): ${unknown.join(' · ')}`, 400);
|
|
81
84
|
}
|
|
85
|
+
stripGatewayEventFields(opRecords);
|
|
82
86
|
const deletedFlows = orphanedFlowPlan(nodes, events, [...opRecords, ...removeEventOps]);
|
|
83
87
|
if (deletedFlows.length > 0 && allowFlowDeletion !== true)
|
|
84
88
|
throw new PlatformApiError(orphanedFlowMessage(deletedFlows), 400);
|
|
@@ -141,14 +145,14 @@ export function registerDesignSchemaPatchTool(server, platform, host) {
|
|
|
141
145
|
response.layoutWarnings = [...layoutWarnings, ...profileWarnings];
|
|
142
146
|
Object.assign(response, applicationTypeField(resolved));
|
|
143
147
|
const contentOps = opRecords.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
|
|
144
|
-
let opDrops =
|
|
148
|
+
let opDrops = emptyWriteDiff();
|
|
145
149
|
if (contentOps.length > 0) {
|
|
146
150
|
try {
|
|
147
151
|
const saved = await fetchHost(platform, host, project, environment, applicationId, id);
|
|
148
152
|
opDrops = schemaOpDrops(saved[spec.tree], contentOps);
|
|
149
153
|
}
|
|
150
154
|
catch {
|
|
151
|
-
opDrops =
|
|
155
|
+
opDrops = emptyWriteDiff();
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
158
|
return withWriteDiff(withNormalization(response, [
|
package/dist/tools/flows.js
CHANGED
|
@@ -5,12 +5,13 @@ import { flowBindings, normalizeFlowSteps, targetlessSetterWarnings, unknownSett
|
|
|
5
5
|
import { flowBodyWarnings } from '../knowledge/coverage.js';
|
|
6
6
|
import { PlatformApiError } from '../platform/errors.js';
|
|
7
7
|
import { handle } from '../platform/responses.js';
|
|
8
|
-
import { writeDiff, withWriteDiff } from '../platform/writeDiff.js';
|
|
8
|
+
import { emptyWriteDiff, writeDiff, withWriteDiff } from '../platform/writeDiff.js';
|
|
9
9
|
import { resolvePatchOperations } from '../knowledge/outlines.js';
|
|
10
10
|
import { patchResult, baseVersionOf } from '../platform/patch.js';
|
|
11
11
|
import fastJsonPatch from 'fast-json-patch';
|
|
12
12
|
import { policyRules, flowExpressionSites } from '../knowledge/rules.js';
|
|
13
13
|
import { functionCallRefs } from '../knowledge/functionGuards.js';
|
|
14
|
+
import { existingMatch, ifExistsParam, platformRename, withRename } from '../platform/ifExists.js';
|
|
14
15
|
import { randomUUID } from 'node:crypto';
|
|
15
16
|
import { flowProfileWarnings } from './profileWrite.js';
|
|
16
17
|
import { z } from 'zod';
|
|
@@ -20,14 +21,18 @@ import { z } from 'zod';
|
|
|
20
21
|
* `flow_create` 200 döndü, `flow_get` adımı gösterdi ama `args` içinde kodu taşıyan alan yoktu —
|
|
21
22
|
* teşhis ~10 dakika sürdü ve tek belirti filtrenin hiçbir şey yapmamasıydı. Okuma başarısız
|
|
22
23
|
* olursa yazma yine başarılıdır: diff bir teşhis eklentisidir, yazmanın sonucu değil.
|
|
24
|
+
*
|
|
25
|
+
* Okunan kayıt da döner: `ifExists:'duplicate'` yazmasında platform ada/slug'a ' (1)' ekler ve yanıtın
|
|
26
|
+
* gönderilen adı değil KAYDEDİLEN adı göstermesi gerekir (flow-upsert-ve-baglar S-10).
|
|
23
27
|
*/
|
|
24
|
-
async function flowWriteDrops(platform, host, project, environment, applicationId, hostId, flowId, sent) {
|
|
28
|
+
async function flowWriteDrops(platform, host, project, environment, applicationId, hostId, flowId, sent, allowRename = false) {
|
|
25
29
|
try {
|
|
26
30
|
const saved = unwrap(await platform.tenant('GET', project, flowPath(host, applicationId, hostId, flowId), { environment }));
|
|
27
|
-
|
|
31
|
+
const rename = allowRename ? platformRename(sent, saved, 'paren') : { sent };
|
|
32
|
+
return { ...writeDiff(rename.sent, saved), saved, ...(rename.renamed ? { renamed: rename.renamed } : {}) };
|
|
28
33
|
}
|
|
29
34
|
catch {
|
|
30
|
-
return
|
|
35
|
+
return emptyWriteDiff();
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
const stepsParam = z.array(z.record(z.unknown())).describe("Adım listesi (StepManagementSchema: { stepId, name, type, kind, args, branches }). DİKKAT: branches[].nextStepId bir GUID değil, hedef adımın stepId STRING'idir; dal ADI motorun anahtarıdır ve start/setter/code/function adımlarında 'always' olmak zorundadır. Tam şablon: ngapps://examples/flow-load-table (ya da flow_get çıktısı); adım tipleri ve setter/parameters şekli platform_guide('page') Kural 1c. Kullanıcı fonksiyonu çağırmak için 'function' adımı: { type:'function', args:{ functionId:'<function_list id>', functionName:'<ad>', parameters:[{ name:'<bildirimdeki ad>', value:{ type:'static'|'property'|'storage'|'state'|'code', ref } }] } } → çıktı Steps.<adımAdı>.data (ngapps://examples/flow-call-function, platform_guide('functions')).");
|
|
@@ -70,8 +75,7 @@ function flowListResult(flows, record) {
|
|
|
70
75
|
...(danglingBindings.length > 0 ? { danglingBindings } : {})
|
|
71
76
|
};
|
|
72
77
|
}
|
|
73
|
-
const
|
|
74
|
-
const upper = (value) => String(value ?? '').toUpperCase();
|
|
78
|
+
const flowIfExistsParam = ifExistsParam('akış', 'mevcut akışı bu gövdeyle günceller (id\'si ve olay bağları korunur)');
|
|
75
79
|
/**
|
|
76
80
|
* Barındırıcının akışları, önbelleksiz liste ucundan (`GET /pages|layouts/{id}/flows`). Layout ve
|
|
77
81
|
* sayfa tasarım kaydı önbellekten gelir ve zarfsız akış yazmasından sonra akışı hemen taşımaz;
|
|
@@ -104,36 +108,21 @@ async function listHostFlows(platform, host, project, environment, applicationId
|
|
|
104
108
|
* tam da zaman aşımında kopya üretirdi.
|
|
105
109
|
*/
|
|
106
110
|
async function existingFlowMatch(platform, host, project, environment, applicationId, hostId, hostRecord, name, slug, flowId, ifExists) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const copies = flows.filter(flow => copyPattern.test(String(flow.name ?? '')));
|
|
123
|
-
const copyNote = copies.length > 0
|
|
124
|
-
? ` Barındırıcıda bu adın sayaçlı eşleri de var: ${copies.map(describe).join(' · ')} — bağı olup olmadığını flow_list (bindings) ile kontrol edip gerekirse silin.`
|
|
125
|
-
: '';
|
|
126
|
-
if (ifExists === 'error') {
|
|
127
|
-
const where = host === 'page' ? 'sayfada' : host === 'layout' ? "layout'ta" : "widget'ta";
|
|
128
|
-
throw new PlatformApiError(`FLOW-026: bu ${where} aynı ad ya da slug'da akış zaten var: ${matches.map(describe).join(' · ')}. İstek gönderilmedi — gönderilseydi platform ürün kuralı gereği ikinci bir akış açardı (çakışan ad ya da slug'a ' (1)' sayacı eklenerek; yalnız harf farkı varsa sayaçsız). Mevcut akışı güncellemek için aynı çağrıyı ifExists:'update' ile tekrarlayın; bilerek ikinci akış istiyorsanız ifExists:'duplicate'.${copyNote}`, 409);
|
|
129
|
-
}
|
|
130
|
-
if (matches.length > 1) {
|
|
131
|
-
throw new PlatformApiError(`FLOW-026: ad ve slug farklı akışlara çıkıyor (${matches.map(describe).join(' · ')}); hangisinin güncelleneceği belirsiz, istek gönderilmedi. flowId ile flow_update kullanın.${copyNote}`, 409);
|
|
132
|
-
}
|
|
133
|
-
if (flowId !== undefined && flowId.toLowerCase() !== String(match.id).toLowerCase()) {
|
|
134
|
-
throw new PlatformApiError(`FLOW-026: verilen flowId (${flowId}) aynı adlı mevcut akışla (${describe(match)}) çelişiyor; istek gönderilmedi. Mevcut akışı güncellemek için flowId'yi vermeyin ya da ${String(match.id)} verin.`, 409);
|
|
135
|
-
}
|
|
136
|
-
return match;
|
|
111
|
+
return await existingMatch({
|
|
112
|
+
code: 'FLOW-026',
|
|
113
|
+
noun: 'akış',
|
|
114
|
+
where: host === 'page' ? 'bu sayfada' : host === 'layout' ? "bu layout'ta" : "bu widget'ta",
|
|
115
|
+
name,
|
|
116
|
+
slug,
|
|
117
|
+
id: flowId,
|
|
118
|
+
idParam: 'flowId',
|
|
119
|
+
ifExists,
|
|
120
|
+
list: () => listHostFlows(platform, host, project, environment, applicationId, hostId, async () => hostRecord),
|
|
121
|
+
listTool: 'flow_list',
|
|
122
|
+
copyCheck: 'bağı olup olmadığını flow_list (bindings) ile kontrol edip gerekirse silin',
|
|
123
|
+
updateTool: 'flowId ile flow_update',
|
|
124
|
+
slugSuffix: 'paren'
|
|
125
|
+
});
|
|
137
126
|
}
|
|
138
127
|
async function requiredParameterWarnings(platform, project, environment, applicationId, steps) {
|
|
139
128
|
const warnings = [];
|
|
@@ -357,7 +346,7 @@ export function registerFlowTools(server, platform) {
|
|
|
357
346
|
steps: stepsParam,
|
|
358
347
|
errorSteps: stepsParam.optional(),
|
|
359
348
|
flowId: guid.optional().describe('Verilmezse platform üretir; şemaya önceden bağlamak için kendiniz üretebilirsiniz'),
|
|
360
|
-
ifExists:
|
|
349
|
+
ifExists: flowIfExistsParam,
|
|
361
350
|
policyOverride: policyOverrideParam
|
|
362
351
|
},
|
|
363
352
|
annotations: writes
|
|
@@ -393,17 +382,22 @@ export function registerFlowTools(server, platform) {
|
|
|
393
382
|
}
|
|
394
383
|
// Diff YALNIZ çağıranın yazdığı alanları kapsar; `id` ve barındırıcı bağı (`pageId` vb.)
|
|
395
384
|
// gövdeye gateway koyar, kullanıcının düzeltebileceği bir şey değildir.
|
|
396
|
-
const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, writtenFlowId, { name: flow.name, slug: flow.slug, steps: flow.steps, errorSteps: flow.errorSteps });
|
|
385
|
+
const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, writtenFlowId, { name: flow.name, slug: flow.slug, steps: flow.steps, errorSteps: flow.errorSteps }, ifExists === 'duplicate');
|
|
397
386
|
const platformWarnings = await flowProfileWarnings(platform, applicationId, [...(flow.steps ?? []), ...(flow.errorSteps ?? [])]);
|
|
398
|
-
return attachFindings(withWriteDiff(withNormalization({
|
|
387
|
+
return attachFindings(withWriteDiff(withRename(withNormalization({
|
|
399
388
|
host,
|
|
400
389
|
hostId,
|
|
401
390
|
outcome: existing === undefined ? 'created' : 'updated',
|
|
402
|
-
flow: {
|
|
391
|
+
flow: {
|
|
392
|
+
id: writtenFlowId,
|
|
393
|
+
name: typeof drops.saved?.name === 'string' ? drops.saved.name : name,
|
|
394
|
+
slug: typeof drops.saved?.slug === 'string' ? drops.saved.slug : slug,
|
|
395
|
+
steps: steps.length
|
|
396
|
+
},
|
|
403
397
|
...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
|
|
404
398
|
...(platformWarnings.length > 0 ? { platformWarnings } : {}),
|
|
405
|
-
hint: "Akışı bir olaya bağlamak için: page_schema_patch (layout'ta layout_schema_patch, widget'ta component_schema_patch) ops:[{op:'set_event', target:<elementId>, event:'<component_get events listesindeki ad>', eventValue:<flow id>}]. Olay adları komponente özeldir ve 'on' ÖNEKİ ALMAZ (button → 'click', form → 'submit', table → 'mount'/'select'/'sort'…); uydurma ad 200 döner ama bağ sessizce ölü kalır — gateway PAGE-008 ile yakalar."
|
|
406
|
-
}, normalized), drops), findings);
|
|
399
|
+
hint: "Akışı bir olaya bağlamak için: page_schema_patch (layout'ta layout_schema_patch, widget'ta component_schema_patch) ops:[{op:'set_event', target:<elementId>, event:'<component_get events listesindeki ad>', eventValue:<flow id>}]; olayda zaten akış bağlıysa ikinciyi add_event ile ekleyin (set_event bağlı akışı değiştirir, PAGE-034). Olay adları komponente özeldir ve 'on' ÖNEKİ ALMAZ (button → 'click', form → 'submit', table → 'mount'/'select'/'sort'…); uydurma ad 200 döner ama bağ sessizce ölü kalır — gateway PAGE-008 ile yakalar."
|
|
400
|
+
}, normalized), drops.renamed), drops), findings);
|
|
407
401
|
}));
|
|
408
402
|
server.registerTool('flow_update', {
|
|
409
403
|
title: 'Akış güncelle',
|
package/dist/tools/functions.js
CHANGED
|
@@ -16,7 +16,7 @@ import { z } from 'zod';
|
|
|
16
16
|
const sourceParam = z.enum(['workflow', 'studio']).describe("Nerede çalışır: 'workflow' → SUNUCU (workflow/middleware/automation ifadeleri, Jint), tenant geneli, çıplak adla çağrılır (toplamHesapla(1, 2)), applicationId/scope/pageId VERİLMEZ, isAsync olamaz. 'studio' → TARAYICI ({{ }} bağlaması, UI flow code/function adımı), uygulamaya ait, Functions.toplamHesapla(1, 2) ile çağrılır, applicationId + scope zorunlu. Kaynak sonradan DEĞİŞMEZ");
|
|
17
17
|
const scopeParam = z.enum(['application', 'page']).describe("Yalnız source:'studio'. application: uygulamanın her sayfasında görünür. page: yalnız o sayfanın tarayıcı bağlamında, pageId zorunlu (K2). Ad uygulama içinde kapsamlar arası benzersizdir (K3)");
|
|
18
18
|
const nameParam = z.string().min(1).describe("Fonksiyon adı; studio'da Functions.<ad>(…), workflow'da <ad>(…) ile çağrılır. JS tanımlayıcısı olmalı, ayrılmış sözcük olamaz; studio'da 'Functions', workflow'da motor global adları (system, input, Now…) yasaktır (FN-001, istek gitmeden). Aynı ad iki kaynakta yan yana yaşayabilir (ayrı ad alanları); aynı kaynak içinde tekrarı platform 422 ile reddeder");
|
|
19
|
-
const bodyParam = z.string().min(1).max(65536).describe("Fonksiyon GÖVDESİ, iki kaynakta aynı biçim: düz JavaScript deyimleri, 'return' içerebilir — 'function' sarmalı ya da arrow YAZILMAZ ('return fiyat * adet'). Gövde çağrıldığı yerin bağlamını kapanış olarak görür: tarayıcıda Page/state/Steps/komponent adları, sunucuda input/adım çıktıları/system.* (K7). Parametre adları gövdede doğrudan kullanılır. Platform kaydetmeden önce SÖZDİZİMİNİ denetler (422 GlobalFunctionBodyInvalid, ayrıştırıcı mesajıyla). En çok 64 KB");
|
|
19
|
+
const bodyParam = z.string().min(1).max(65536).describe("Fonksiyon GÖVDESİ, iki kaynakta aynı biçim: düz JavaScript deyimleri, 'return' içerebilir — 'function' sarmalı ya da arrow YAZILMAZ ('return fiyat * adet'). Gövde çağrıldığı yerin bağlamını kapanış olarak görür: tarayıcıda Page/state/Steps/komponent adları, sunucuda input/adım çıktıları/system.* (K7). Parametre adları gövdede doğrudan kullanılır. Gövde YORUMLU yazılır: başta ne yaptığını/parametreleri/dönüşü söyleyen // baş yorumu, açık olmayan adımlarda kısa satır yorumu; 5+ dolu satırlı yorumsuz gövde FN-006 uyarısı alır (Studio yorumu yeşil çizer). Platform kaydetmeden önce SÖZDİZİMİNİ denetler (422 GlobalFunctionBodyInvalid, ayrıştırıcı mesajıyla). En çok 64 KB");
|
|
20
20
|
const parameterParam = z.object({
|
|
21
21
|
name: z.string().min(1).describe('Parametre adı — JS tanımlayıcısı, fonksiyon içinde benzersiz; gövdede bu adla okunur'),
|
|
22
22
|
type: z.enum(['text', 'number', 'boolean', 'date', 'object', 'array', 'any']).optional()
|
|
@@ -64,7 +64,10 @@ export function describeSignatureChange(current, next) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
function signatureWarning(name, source, change, dependents) {
|
|
67
|
-
const callers = [
|
|
67
|
+
const callers = ['flows', 'workflows', 'bindings', 'functions']
|
|
68
|
+
.map(bucket => (dependents[bucket].length ? `${bucket}: ${dependents[bucket].join(', ')}` : ''))
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join('; ');
|
|
68
71
|
const what = [
|
|
69
72
|
change.name ? `ad '${change.name[0]}' → '${change.name[1]}'` : '',
|
|
70
73
|
change.parameters ? `parametreler [${change.parameters[0].join(', ')}] → [${change.parameters[1].join(', ')}]` : ''
|
|
@@ -75,11 +78,12 @@ function signatureWarning(name, source, change, dependents) {
|
|
|
75
78
|
return `İmza değişti (${what}) ve fonksiyonu ${dependents.total} kayıt kullanıyor (${callers}): ${effect}. Güncelleme yapıldı; çağıran ifadeleri gözden geçirin — dependency_graph(module:'global-function').`;
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
|
-
* `GET global-functions/{id}/dependencies` KOVALI grafiği döner (`{ flows, workflows }`); düz dizi
|
|
81
|
+
* `GET global-functions/{id}/dependencies` KOVALI grafiği döner (`{ flows, workflows, bindings, functions }`); düz dizi
|
|
79
82
|
* şekli de düzleştirilir — yapısal (`args.functionId`) ve metinsel (`Functions.<ad>(` / çıplak `<ad>(`)
|
|
80
83
|
* bağımlılıklar aynı listede gelir. Etiket Studio rozetleriyle aynıdır: komponent olayı `Sahip › Komponent · olay`,
|
|
81
84
|
* sayfa/layout olayı `Sahip · olay`, bağı olmayan akış `Sahip › akış adı` (sahip sayfa/layout/widget; eski yanıtta
|
|
82
|
-
* sahipsiz); automation düğümü `ad (automation)`.
|
|
85
|
+
* sahipsiz); automation düğümü `ad (automation)`. Kod tipli prop bağlaması `Sahip › Komponent · prop`, gövdesinde
|
|
86
|
+
* çağıran fonksiyon adıyla yazılır.
|
|
83
87
|
*/
|
|
84
88
|
export function summarizeDependents(payload) {
|
|
85
89
|
const data = (() => {
|
|
@@ -94,7 +98,7 @@ export function summarizeDependents(payload) {
|
|
|
94
98
|
const name = String(entry.name ?? entry.slug ?? entry.id ?? '?');
|
|
95
99
|
const host = text(entry.hostName);
|
|
96
100
|
const component = text(entry.componentName);
|
|
97
|
-
const event = text(entry.eventTitle) ?? text(entry.eventName);
|
|
101
|
+
const event = text(entry.eventTitle) ?? text(entry.eventName) ?? text(entry.propertyName);
|
|
98
102
|
const label = component
|
|
99
103
|
? [host, [component, event].filter(Boolean).join(' · ')].filter(Boolean).join(' › ')
|
|
100
104
|
: event
|
|
@@ -106,14 +110,20 @@ export function summarizeDependents(payload) {
|
|
|
106
110
|
const entries = data.filter(isRecord);
|
|
107
111
|
const moduleOf = (entry) => String(entry.module ?? entry.type ?? entry.kind ?? '').toLowerCase();
|
|
108
112
|
const isWorkflow = (entry) => moduleOf(entry).includes('workflow') || moduleOf(entry) === 'automation';
|
|
113
|
+
const isFunction = (entry) => moduleOf(entry) === 'global-function';
|
|
114
|
+
const isBinding = (entry) => text(entry.propertyName) !== undefined;
|
|
109
115
|
const workflows = names(entries.filter(isWorkflow));
|
|
110
|
-
const
|
|
111
|
-
|
|
116
|
+
const functions = names(entries.filter(isFunction));
|
|
117
|
+
const bindings = names(entries.filter(entry => !isWorkflow(entry) && !isFunction(entry) && isBinding(entry)));
|
|
118
|
+
const flows = names(entries.filter(entry => !isWorkflow(entry) && !isFunction(entry) && !isBinding(entry)));
|
|
119
|
+
return { flows, workflows, bindings, functions, total: flows.length + workflows.length + bindings.length + functions.length };
|
|
112
120
|
}
|
|
113
121
|
const record = isRecord(data) ? data : {};
|
|
114
122
|
const flows = names(record.flows);
|
|
115
123
|
const workflows = names(record.workflows);
|
|
116
|
-
|
|
124
|
+
const bindings = names(record.bindings);
|
|
125
|
+
const functions = names(record.functions);
|
|
126
|
+
return { flows, workflows, bindings, functions, total: flows.length + workflows.length + bindings.length + functions.length };
|
|
117
127
|
}
|
|
118
128
|
function usageHint(name, source, scope) {
|
|
119
129
|
if (source === 'workflow') {
|
|
@@ -268,7 +278,7 @@ export function registerFunctionTools(server, platform) {
|
|
|
268
278
|
data: saved,
|
|
269
279
|
signature: functionSignature(saved.name ? saved : definition),
|
|
270
280
|
signatureChange,
|
|
271
|
-
dependents: dependents ?? { flows: [], workflows: [], total: 0 },
|
|
281
|
+
dependents: dependents ?? { flows: [], workflows: [], bindings: [], functions: [], total: 0 },
|
|
272
282
|
...(dependents === undefined ? { dependentsNote: 'Bağımlılık ucu okunamadı; çağıran akış olup olmadığı bilinmiyor.' } : {}),
|
|
273
283
|
...(renamedReferences !== undefined ? { renamedReferences } : {}),
|
|
274
284
|
...(dependents !== undefined && dependents.total > 0 && callersBreak
|
|
@@ -321,7 +331,7 @@ export function registerFunctionTools(server, platform) {
|
|
|
321
331
|
return {
|
|
322
332
|
deleted: id,
|
|
323
333
|
source,
|
|
324
|
-
dependents: dependents ?? { flows: [], workflows: [], total: 0 },
|
|
334
|
+
dependents: dependents ?? { flows: [], workflows: [], bindings: [], functions: [], total: 0 },
|
|
325
335
|
...(dependents === undefined ? { dependentsNote: 'Bağımlılık ucu okunamadı; çağıran akış olup olmadığı bilinmiyor.' } : {}),
|
|
326
336
|
...(dependents !== undefined && dependents.total > 0
|
|
327
337
|
? { note: `${dependents.total} çağıran kırıldı (acknowledgeDependents): ${[...dependents.flows, ...dependents.workflows].join(', ')} — ifadeleri düzeltin ya da fonksiyonu aynı adla yeniden tanımlayın.` }
|
|
@@ -10,11 +10,15 @@ import { policyRules } from '../knowledge/rules.js';
|
|
|
10
10
|
import { writeDiff, withWriteDiff } from '../platform/writeDiff.js';
|
|
11
11
|
import { identityHeaders } from '../platform/tokenVault.js';
|
|
12
12
|
import { PlatformApiError } from '../platform/errors.js';
|
|
13
|
+
import { existingMatch, ifExistsParam, listAll, platformRename, withOutcome, withRename } from '../platform/ifExists.js';
|
|
14
|
+
import { outlineWrite, recordWriteViewParam } from '../platform/writeView.js';
|
|
13
15
|
import { handle } from '../platform/responses.js';
|
|
14
16
|
import fastJsonPatch from 'fast-json-patch';
|
|
15
17
|
import { z } from 'zod';
|
|
16
18
|
import { constraintSummary } from '../knowledge/backendConstraints.js';
|
|
17
19
|
import { unmappedMembers, introducedUnmappedMembers, withUnmappedMembers } from '../knowledge/schemaGuard.js';
|
|
20
|
+
const MICROSERVICE_OUTLINE_KEYS = ['url', 'httpMethod', 'source', 'type'];
|
|
21
|
+
const microserviceIfExistsParam = ifExistsParam('uç', 'mevcut ucu bu tanımla tam gövde günceller (id\'si korunur; microservice_update ile aynı yazma)');
|
|
18
22
|
const policyOverrideParam = z.boolean().optional()
|
|
19
23
|
.describe('true: politika block bulgularını warn\'a indirir (bilinçli istisna; yanıtta işaretlenir)');
|
|
20
24
|
/**
|
|
@@ -287,28 +291,52 @@ export function registerMicroserviceTools(server, platform) {
|
|
|
287
291
|
}));
|
|
288
292
|
server.registerTool('microservice_create', {
|
|
289
293
|
title: 'Microservice oluştur',
|
|
290
|
-
description: "Yeni microservice tanımlar (definition = CreateMicroserviceCommand). İlk kullanımdan önce platform_guide('microservice') okuyun: kaynak seçimi (table > query) ve selectFields disiplini. 'description' ZORUNLUDUR (MS-014, block): çağıranın bu uçtan ne elde ettiğini yazın. Auth/oturum gerektiren uçlarda definition.middlewareIds ile middleware bağlayın; kimlik kapsaması filtrede source:'expression' + value:'user.id' ile yapılır (platform_guide('middleware')). BU YALNIZ source:'table' UÇLARDA VAR: query kaynaklı ucun parametreleri ÇAĞRIDAN gelir (body → query-string → header) ve middleware'in kurduğu user o yola girmez — kapsamı gövdeye bırakan uç sahtelenebilir (ölçüldü 2026-09-07). Kapsanmış agregasyon ucu sorguyu bir workflow query adımından çağırır: platform_guide('microservice') §'Kimlik kapsaması × uç kaynağı'. Ucun gerçek bir tuzağı varsa (auth istemiyor, sayfalama yok, atomik değil) 'caution' alanına yazın — OPSİYONELDİR, tuzak yoksa boş bırakın. Gövde politika kontrolünden geçer. " + constraintSummary('CreateMicroserviceCommand'),
|
|
291
|
-
inputSchema: { project: projectParam, environment: environmentParam, applicationId: appRef, definition: definitionParam, policyOverride: policyOverrideParam },
|
|
294
|
+
description: "Yeni microservice tanımlar (definition = CreateMicroserviceCommand). Aynı ad ya da slug'da uç varsa varsayılan olarak YAZMAZ ve mevcut ucun id'sini döner (MS-023 — gönderilseydi platform '<ad> (1)' adıyla ikinci bir uç açardı); tekrar çalıştırılabilir yazma için ifExists:'update' verin, yanıttaki outcome 'created' | 'updated' olur. Mevcut uçlar okunamazsa da yazmaz. İlk kullanımdan önce platform_guide('microservice') okuyun: kaynak seçimi (table > query) ve selectFields disiplini. 'description' ZORUNLUDUR (MS-014, block): çağıranın bu uçtan ne elde ettiğini yazın. Auth/oturum gerektiren uçlarda definition.middlewareIds ile middleware bağlayın; kimlik kapsaması filtrede source:'expression' + value:'user.id' ile yapılır (platform_guide('middleware')). BU YALNIZ source:'table' UÇLARDA VAR: query kaynaklı ucun parametreleri ÇAĞRIDAN gelir (body → query-string → header) ve middleware'in kurduğu user o yola girmez — kapsamı gövdeye bırakan uç sahtelenebilir (ölçüldü 2026-09-07). Kapsanmış agregasyon ucu sorguyu bir workflow query adımından çağırır: platform_guide('microservice') §'Kimlik kapsaması × uç kaynağı'. Ucun gerçek bir tuzağı varsa (auth istemiyor, sayfalama yok, atomik değil) 'caution' alanına yazın — OPSİYONELDİR, tuzak yoksa boş bırakın. Gövde politika kontrolünden geçer. " + constraintSummary('CreateMicroserviceCommand'),
|
|
295
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: appRef, definition: definitionParam, ifExists: microserviceIfExistsParam, view: recordWriteViewParam, policyOverride: policyOverrideParam },
|
|
292
296
|
annotations: writes
|
|
293
|
-
}, handle(config, async ({ project, environment, applicationId, definition, policyOverride }) => {
|
|
297
|
+
}, handle(config, async ({ project, environment, applicationId, definition, ifExists, view, policyOverride }) => {
|
|
294
298
|
const resolver = new RefResolver(platform, project, environment);
|
|
295
299
|
const resolvedApplicationId = await resolver.applicationId(applicationId);
|
|
296
300
|
const hydrated = await hydrateMicroserviceRefs(definition, resolver);
|
|
297
301
|
const normalized = normalizeMicroserviceDefinition(definition);
|
|
302
|
+
// Politika ÖNCE koşar: block alan istek platforma hiç gitmez, var-mı listesi de okunmaz.
|
|
303
|
+
const createFindings = enforcePolicy(evaluatePolicy(config, policyRules, 'microservice', definition, { operation: 'create' }, policyOverride));
|
|
304
|
+
const scopeFindings = await queryScopeFindings(platform, project, environment, definition);
|
|
305
|
+
const existing = await existingMatch({
|
|
306
|
+
code: 'MS-023',
|
|
307
|
+
noun: 'uç',
|
|
308
|
+
where: 'bu uygulamada',
|
|
309
|
+
name: typeof definition.name === 'string' ? definition.name : undefined,
|
|
310
|
+
slug: typeof definition.slug === 'string' ? definition.slug : undefined,
|
|
311
|
+
id: typeof definition.id === 'string' ? definition.id : undefined,
|
|
312
|
+
idParam: 'definition.id',
|
|
313
|
+
ifExists,
|
|
314
|
+
list: () => listAll(platform, project, environment, `/microservices/${resolvedApplicationId}`),
|
|
315
|
+
listTool: 'microservice_list',
|
|
316
|
+
copyCheck: 'kullanılıp kullanılmadığını dependency_graph ile kontrol edip gerekirse silin',
|
|
317
|
+
updateTool: 'id ile microservice_update'
|
|
318
|
+
});
|
|
319
|
+
// Kayıt bulunduysa yazma bir UPDATE'tir: gövde kuralları microservice_update ile aynı kipte (R-1).
|
|
298
320
|
const findings = [
|
|
299
|
-
...
|
|
300
|
-
|
|
321
|
+
...(existing === undefined
|
|
322
|
+
? createFindings
|
|
323
|
+
: enforcePolicy(evaluatePolicy(config, policyRules, 'microservice', definition, { operation: 'update' }, policyOverride))),
|
|
324
|
+
...scopeFindings
|
|
301
325
|
];
|
|
302
|
-
const drift = unmappedMembers('CreateMicroserviceCommand', definition);
|
|
303
|
-
const payload =
|
|
304
|
-
|
|
326
|
+
const drift = unmappedMembers(existing === undefined ? 'CreateMicroserviceCommand' : 'UpdateMicroserviceCommand', definition);
|
|
327
|
+
const payload = existing === undefined
|
|
328
|
+
? await platform.tenant('POST', project, `/microservices/${resolvedApplicationId}`, { environment, body: definition })
|
|
329
|
+
: await platform.tenant('PUT', project, `/microservices/${resolvedApplicationId}/${String(existing.id)}`, { environment, body: definition });
|
|
330
|
+
const saved = payload?.data;
|
|
331
|
+
const rename = ifExists === 'duplicate' ? platformRename(definition, saved, 'dash') : { sent: definition };
|
|
332
|
+
return attachFindings(withUnmappedMembers(withWriteDiff(withResolved(withRename(withNormalization(withOutcome(outlineWrite(payload, view, MICROSERVICE_OUTLINE_KEYS), existing), normalized), rename.renamed), [...resolver.resolved, ...hydrated]), writeDiff(rename.sent, payload)), drift), findings);
|
|
305
333
|
}));
|
|
306
334
|
server.registerTool('microservice_update', {
|
|
307
335
|
title: 'Microservice güncelle',
|
|
308
336
|
description: 'Microservice tanımını günceller (tam gövde). Hedefli değişiklik için microservice_patch tercih edin. \'description\' ZORUNLUDUR (MS-014, block). Ucun gerçek bir tuzağı varsa (auth istemiyor, sayfalama yok, atomik değil) \'caution\' alanına yazın — OPSİYONELDİR, tuzak yoksa boş bırakın. Gövde politika kontrolünden geçer. ' + constraintSummary('UpdateMicroserviceCommand'),
|
|
309
|
-
inputSchema: { project: projectParam, environment: environmentParam, applicationId: appRef, id: guid, definition: definitionParam, policyOverride: policyOverrideParam },
|
|
337
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: appRef, id: guid, definition: definitionParam, view: recordWriteViewParam, policyOverride: policyOverrideParam },
|
|
310
338
|
annotations: updates
|
|
311
|
-
}, handle(config, async ({ project, environment, applicationId, id, definition, policyOverride }) => {
|
|
339
|
+
}, handle(config, async ({ project, environment, applicationId, id, definition, view, policyOverride }) => {
|
|
312
340
|
const resolver = new RefResolver(platform, project, environment);
|
|
313
341
|
const resolvedApplicationId = await resolver.applicationId(applicationId);
|
|
314
342
|
const hydrated = await hydrateMicroserviceRefs(definition, resolver);
|
|
@@ -319,7 +347,7 @@ export function registerMicroserviceTools(server, platform) {
|
|
|
319
347
|
];
|
|
320
348
|
const drift = unmappedMembers('UpdateMicroserviceCommand', definition);
|
|
321
349
|
const payload = await platform.tenant('PUT', project, `/microservices/${resolvedApplicationId}/${id}`, { environment, body: definition });
|
|
322
|
-
return attachFindings(withUnmappedMembers(withWriteDiff(withResolved(withNormalization(payload, normalized), [...resolver.resolved, ...hydrated]), writeDiff(definition, payload)), drift), findings);
|
|
350
|
+
return attachFindings(withUnmappedMembers(withWriteDiff(withResolved(withNormalization(outlineWrite(payload, view, MICROSERVICE_OUTLINE_KEYS), normalized), [...resolver.resolved, ...hydrated]), writeDiff(definition, payload)), drift), findings);
|
|
323
351
|
}));
|
|
324
352
|
server.registerTool('microservice_patch', {
|
|
325
353
|
title: 'Microservice\'e patch uygula',
|
package/dist/tools/mobile.js
CHANGED
|
@@ -72,7 +72,7 @@ export function registerMobileTools(server, platform) {
|
|
|
72
72
|
color: z.string().optional(),
|
|
73
73
|
pages: z.array(z.object({ pageSlug: z.string().min(1), pageName: z.string().min(1) })).min(1),
|
|
74
74
|
tabs: z.array(z.object({ pageSlug: z.string().min(1), label: z.string().optional(), icon: z.string().optional() })).optional()
|
|
75
|
-
.describe(`1-${MAX_MOBILE_TABS} sekme; her pageSlug pages içinden. Verilmezse sekme çubuğu yok (yığın).`),
|
|
75
|
+
.describe(`1-${MAX_MOBILE_TABS} sekme; her pageSlug pages içinden. Verilmezse sekme çubuğu yok (yığın). label 't:<anahtar>' ile yazılırsa çeviri sözlüğünden çözülür (platform_guide('mobile') §Çok dil).`),
|
|
76
76
|
drawer: z.boolean().optional().describe("true: layout'a drawer widget'ı + app-bar.menuTarget (çekmece)"),
|
|
77
77
|
title: z.string().optional().describe('app-bar başlığı; boşsa her sayfanın adı yazılır'),
|
|
78
78
|
homepage: z.string().optional().describe('Ana sayfanın pageSlug\'ı; varsayılan ilk sekme, sekme yoksa ilk sayfa'),
|