@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.
@@ -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 (`page-header` default `h:8`, `maxSize` `h:4`) — varsayılanı körü
101
- körüne kopyalama. Sınır grid kanvasının kuralıdır; API 2026-09-15'ten beri reddetmez, ama aşan
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 `page-header`
167
- ve `datatable` yalnız `mount` yayar (`datatable`'ınki bilinçli — Kural 1e).
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. Sayfa düzeyi olaylar için
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 **2026-08-27 kuşağı komponentler** (`eventcalendar` ·
193
- `importwizard` · `kanban` · `signature` · `stepper`): bu kuşak `params`'ı da gönderir, yani
194
- yükleri `event.data.<anahtar>` ile okunur; `kanban` ve `eventcalendar` yükü `self`'e HİÇ
195
- yazmaz (`component_get` çıktısındaki sözleşme notları ayrımı söyler). Sessiz hata örneği:
196
- sayfalama akışını `event.data.limit`'e bağlarsan sorgu `LIMIT null OFFSET null` ile koşar ve
197
- **tüm satırları** döndürür; hata yok, sayfalama yok.
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 }` | `item` / `Row` satır bağlamı |
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 sayfa yüklenirken çözülür ve canlı dil değişimini izlemez (`platform_guide('translation')`) |
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` tazeleme kancasıdır, `month` 1-tabanlı) |
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ığı SAYFA YÜKLENİRKEN çözülür, canlı dil değişimini İZLEMEZ.** Ölçüldü 2026-09-08
179
- > (yerel runtime): `language-selector` ile dil değiştirildiğinde sayfa içeriği ve layout başlığı
180
- > anında yeni dile geçiyor, sekme başlığı ise açılıştaki dilde kalıyor; aynı dille sayfa yeniden
181
- > yüklendiğinde başlık doğru geliyor. Sebep `Runtime.svelte`'te dilin bir `const` olarak bir kez
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`) web ile aynı akış motorunu ve dil seçimini kullanır; cihazda ölçülene kadar kanıtlı olan web'dir.
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": "05c7864d58e308973805f7becee1578ce9eddad196abcdf7eebb43b1e7677549",
3
- "generatedAt": "2026-09-19T20:19:15.052Z",
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, [
@@ -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
- return writeDiff(sent, saved);
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 { dropped: [], normalized: [] };
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 ifExistsParam = z.enum(['error', 'update', 'duplicate']).default('error').describe("Barındırıcıda aynı ad ya da slug'da (büyük/küçük harf duyarsız) akış varsa ne olacağı. 'error' (varsayılan): yazmaz, mevcut akışın id'sini döner. 'update': mevcut akışı bu gövdeyle günceller (id'si ve olay bağları korunur). 'duplicate': platformun ürün davranışı — ikinci kayıt açılır, platform ada/slug'a ' (1)' sayacını ekler (Studio'daki 'New Flow (1)' ile aynı kural).");
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
- if (ifExists === 'duplicate')
108
- return undefined;
109
- let flows;
110
- try {
111
- flows = await listHostFlows(platform, host, project, environment, applicationId, hostId, async () => hostRecord);
112
- }
113
- catch (error) {
114
- throw new PlatformApiError(`FLOW-026: barındırıcının mevcut akışları okunamadı, akış YAZILMADI (${error instanceof Error ? error.message : String(error)}). Liste okunmadan yazmak aynı adla ikinci akış açabilir; flow_list ile tekrar deneyin ya da bilerek ikinci akış istiyorsanız ifExists:'duplicate' verin.`);
115
- }
116
- const matches = flows.filter(flow => upper(flow.name) === upper(name) || upper(flow.slug) === upper(slug));
117
- const [match] = matches;
118
- if (match === undefined)
119
- return undefined;
120
- const describe = (flow) => `${String(flow.id)} ('${String(flow.name)}', slug '${String(flow.slug)}')`;
121
- const copyPattern = new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} \\(\\d+\\)$`, 'i');
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: ifExistsParam,
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: { id: writtenFlowId, name, slug, steps: steps.length },
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',
@@ -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 = [dependents.flows.length ? `flows: ${dependents.flows.join(', ')}` : '', dependents.workflows.length ? `workflows: ${dependents.workflows.join(', ')}` : ''].filter(Boolean).join('; ');
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 flows = names(entries.filter(entry => !isWorkflow(entry)));
111
- return { flows, workflows, total: flows.length + workflows.length };
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
- return { flows, workflows, total: flows.length + workflows.length };
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
- ...enforcePolicy(evaluatePolicy(config, policyRules, 'microservice', definition, { operation: 'create' }, policyOverride)),
300
- ...await queryScopeFindings(platform, project, environment, definition)
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 = await platform.tenant('POST', project, `/microservices/${resolvedApplicationId}`, { environment, body: definition });
304
- return attachFindings(withUnmappedMembers(withWriteDiff(withResolved(withNormalization(payload, normalized), [...resolver.resolved, ...hydrated]), writeDiff(definition, payload)), drift), findings);
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',
@@ -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'),