@appaflytech/zeyrek-ai-mcp 0.0.9 → 0.0.11

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.
@@ -252,7 +252,9 @@ const BASE_TOKENS = {
252
252
  };
253
253
  export const TOKENS = BASE_TOKENS;
254
254
  /** Palet gruplarının anahtarları — paletin KENDİSİNDEN türer ki listeyi ikinci kez yazmayalım. */
255
- export const PALETTE_GROUPS = Object.fromEntries(Object.entries(WHITE_PALETTE).map(([group, values]) => [group, Object.keys(values)]));
255
+ /** Tabanda BULUNMAYAN, yalnız override olarak yazılabilen anahtarlar (f-08: `accent.onPrimary` → `--ng-text-on-accent`). */
256
+ export const OPTIONAL_PALETTE_KEYS = { accent: ['onPrimary'] };
257
+ export const PALETTE_GROUPS = Object.fromEntries(Object.entries(WHITE_PALETTE).map(([group, values]) => [group, [...Object.keys(values), ...(OPTIONAL_PALETTE_KEYS[group] ?? [])]]));
256
258
  /* ---- Birleştiriciler ve ölçekler (`utils.ts` kopyası) ---- */
257
259
  export function mergePalette(base, overrides) {
258
260
  const result = { ...base };
@@ -685,7 +687,7 @@ export function buildThemeStyle(palette, form, t, shadow, formDefaultSize) {
685
687
  --ng-action-yellow: ${actions.yellow || colors.yellow.r600};
686
688
  --ng-action-white: ${actions.white || palette.background.surface};
687
689
  --ng-action-black: ${actions.black || colors.grey.r900};
688
- --ng-text-on-accent: ${getContrastColor(palette.accent.primary)};
690
+ --ng-text-on-accent: ${palette.accent.onPrimary || getContrastColor(palette.accent.primary)};
689
691
  `;
690
692
  }
691
693
  /** Temanın yaydığı değişken adlarının tam listesi — elle tutulmaz, gerçek şablondan türer. */
@@ -0,0 +1,157 @@
1
+ {
2
+ "_meta": {
3
+ "synthetic": false,
4
+ "note": "Figma dosyasından 2026-09-16 tek geçiş protokolüyle okunan gerçek anlık görüntü: 8 çağrı (get_metadata sayfa listesi · get_variable_defs iki frame · get_metadata Deals frame ağacı · get_screenshot Deals · get_design_context buton ve durum rozeti, excludeScreenshot) ≈ 29 bin karakter. Kit iki token dili taşır (Fonts/*, Light/Basic/*); hints ajanın kararıdır ve dosyada kalır — ikinci oturum Figma'ya dönmez. Gölge (effects) okunmadı, alan yok.",
5
+ "usage": "Gövde <themeDiscoverArgs.snapshot> theme_discover'a verilir; öneri theme_update({ theme, dryRun: true }) ile önizlenir, sonra yazılır. Dosyanın yeri: zeyrek-mcp/docs/<koşu>/design/<fileKey>.snapshot.json."
6
+ },
7
+ "_rules": [
8
+ "Bütçe: tasarım dosyası başına ≤ 8 Figma çağrısı, bağlama giren ≤ 150 bin karakter; get_design_context yalnız atomik bileşene (excludeScreenshot: true), ekran frame'ine asla.",
9
+ "variables get_variable_defs çıktısının kendisidir — ad yeniden yazılmaz, değer olduğu gibi taşınır (Font(...) değerleri metin stili olarak okunur); eşleme adı okur.",
10
+ "Eşlenmeyen token için Figma'ya dönülmez: karar hints'e yazılır (tema yolu ya da ignore). Sayfa düzeyinde get_variable_defs seçim ister — frame id verin.",
11
+ "Koyu palet aydınlıktan türetilmez: variablesDark yoksa paletteDark boşluk kalır (THEME-009); isteğe bağlı deriveDark: true başlangıç noktası üretir."
12
+ ],
13
+ "themeDiscoverArgs": {
14
+ "snapshot": {
15
+ "source": {
16
+ "kind": "figma",
17
+ "fileKey": "9NnUskJSeNkMy1gAU2fAaA",
18
+ "fileName": "Recurring | Saas Company Management System (Community) (Copy)",
19
+ "nodeIds": [
20
+ "0:1",
21
+ "92:4339",
22
+ "79:8715",
23
+ "92:4343",
24
+ "92:5596"
25
+ ],
26
+ "fetchedAt": "2026-09-16",
27
+ "calls": 8,
28
+ "characters": 29000
29
+ },
30
+ "variables": {
31
+ "Primary": "#4787FA",
32
+ "Fonts/Primary": "#25213B",
33
+ "Fonts/Primary Variant": "#6E6893",
34
+ "Fonts/Primary Icon": "#8B83BA",
35
+ "Fonts/Positive": "#007F00",
36
+ "Backgrounds/Stripe": "#F4F2FF",
37
+ "White Primary": "#F3F4F8",
38
+ "Table/Footer": "Font(family: \"Inter\", style: Semi Bold, size: 12, weight: 600, lineHeight: 100, letterSpacing: 5)",
39
+ "Light/Basic/Primary": "#242731",
40
+ "Light/Basic/Secondary": "#575F6E",
41
+ "Light/Basic/Thriary": "#79808F",
42
+ "Light/Basic/Strokes": "#D4D4D4",
43
+ "Light/Basic/White": "#FFFFFF",
44
+ "Light/Accent/Primary": "#4277FF",
45
+ "Light/Background/Secondary": "#E3EBFF",
46
+ "Light/Background/Contrast": "#242731",
47
+ "Light/Invertion/Primary": "#FFFFFF"
48
+ },
49
+ "textStyles": [
50
+ {
51
+ "name": "Button label",
52
+ "fontFamily": "DM Sans",
53
+ "fontSize": 14,
54
+ "fontWeight": 700
55
+ },
56
+ {
57
+ "name": "Status pill",
58
+ "fontFamily": "Inter",
59
+ "fontSize": 12,
60
+ "fontWeight": 500
61
+ }
62
+ ],
63
+ "layout": {
64
+ "desktopWidth": 1440,
65
+ "mobileWidth": 375,
66
+ "sidebarWidth": 94,
67
+ "notes": [
68
+ "Kabuk (get_metadata 92:4339): solda 94 px 'Admin Nav' rayı, üstte 76 px 'Admin Top Nav'; içerik x=94'ten başlar, 1345 px genişlik",
69
+ "Liste (Deals): 'Headers' şeridi 76 px — solda 39 px yüksekliğinde başlık metni, sağda hap biçimli 202×46 px birincil buton; altında 48 px sekme şeridi (All · Paid · Unpaid · Overdue, aktif sekmenin altında 2 px çizgi) ve sağda toplam tutar; 45 px tablo başlığı (büyük harf); 61 px satırlar; satırda durum pill'i (19 px, önünde 6 px nokta), 'View More' ikincil buton 71×25, satır sonu 'Shape/More'; altta 53 px Pagination",
70
+ "Form (Create Client Modal 79:8715) iki token dili taşıyor: 'Light/Basic/*' metin tonları, 'Light/Accent/Primary' #4277FF (Deals'in #4787FA'sından farklı ikinci bir accent) — kit iki tasarım sisteminin karışımı",
71
+ "Masaüstü ızgarası iki nesil: eski ekranlar 1440×1024, yeni İK ekranları 1728×1117 — 1440 taban alındı"
72
+ ]
73
+ },
74
+ "components": {
75
+ "button/primary": {
76
+ "background": "#4787FA",
77
+ "color": "#FFFFFF",
78
+ "borderRadius": 23,
79
+ "height": 46,
80
+ "paddingX": 29,
81
+ "paddingY": 14,
82
+ "fontSize": 14,
83
+ "fontWeight": 700
84
+ },
85
+ "badge/status": {
86
+ "background": "#E6E6F2",
87
+ "color": "#4A4AFF",
88
+ "borderRadius": 10,
89
+ "height": 19,
90
+ "paddingX": 18,
91
+ "paddingY": 2,
92
+ "fontSize": 12,
93
+ "fontWeight": 500
94
+ }
95
+ },
96
+ "screens": [
97
+ {
98
+ "nodeId": "92:4339",
99
+ "name": "Deals",
100
+ "role": "list",
101
+ "notes": [
102
+ "sekme filtreleri + toplam tutar vurgusu + pill rozetler; 11 satır + Pagination"
103
+ ]
104
+ },
105
+ {
106
+ "nodeId": "74:4098",
107
+ "name": "Clients",
108
+ "role": "list"
109
+ },
110
+ {
111
+ "nodeId": "79:8715",
112
+ "name": "Create Client Modal",
113
+ "role": "form"
114
+ },
115
+ {
116
+ "nodeId": "310:5445",
117
+ "name": "grant leave",
118
+ "role": "form"
119
+ },
120
+ {
121
+ "nodeId": "310:3809",
122
+ "name": "employee profile",
123
+ "role": "detail"
124
+ },
125
+ {
126
+ "nodeId": "230:3757",
127
+ "name": "Login",
128
+ "role": "auth",
129
+ "notes": [
130
+ "767 px mobil varyantı var"
131
+ ]
132
+ },
133
+ {
134
+ "nodeId": "8:3389",
135
+ "name": "settings",
136
+ "role": "other"
137
+ }
138
+ ],
139
+ "hints": {
140
+ "Light/Accent/Primary": "ignore",
141
+ "Light/Basic/Primary": "ignore",
142
+ "Light/Basic/Secondary": "ignore",
143
+ "Fonts/Primary Variant": "text.secondary",
144
+ "Fonts/Primary Icon": "ignore",
145
+ "Fonts/Positive": "status.success",
146
+ "Backgrounds/Stripe": "background.surfaceAlt",
147
+ "White Primary": "background.canvas",
148
+ "Light/Basic/Thriary": "text.disabled",
149
+ "Light/Basic/Strokes": "border.primary",
150
+ "Light/Basic/White": "background.surface",
151
+ "Light/Background/Secondary": "accent.subtle",
152
+ "Light/Background/Contrast": "background.inverse",
153
+ "Light/Invertion/Primary": "accent.onPrimary"
154
+ }
155
+ }
156
+ }
157
+ }
@@ -18,7 +18,7 @@ yazmalarının aksine burada "önce development'ta dene" diye bir ayrım YOKTUR.
18
18
  | Alan | Ne yapar | Ürettiği değişken(ler) |
19
19
  |---|---|---|
20
20
  | `mode` | `'light'` (varsayılan) \| `'dark'` — yayınlanan uygulamanın modu | palet tabanını seçer |
21
- | `palette` | AYDINLIK moddaki renk override'ları | `--ng-accent-*` · `--ng-background-*` · `--ng-text-*` · `--ng-border-*` · `--ng-status-*` |
21
+ | `palette` | AYDINLIK moddaki renk override'ları; `accent.onPrimary` isteğe bağlıdır — accent ÜSTÜNDEKİ metin, verilmezse kontrasttan türer (`#4787FA` gibi açık bir mavi siyah metin üretir, tasarımda beyazsa buraya yazın) | `--ng-accent-*` · `--ng-background-*` · `--ng-text-*` · `--ng-border-*` · `--ng-status-*` · `--ng-text-on-accent` |
22
22
  | `paletteDark` | KARANLIK moddaki renk override'ları | aynı küme |
23
23
  | `fontFamily` · `headingFontFamily` | Gövde ve başlık yazı tipi (Google Fonts adı) | `--ng-font` · `--ng-font-heading` |
24
24
  | `fontSize` · `fontWeight` | Taban punto ve ağırlık | `--ng-font-size` · `--ng-font-weight` |
@@ -208,3 +208,86 @@ kareyi de alın: `page_screenshot(…)` ve `page_screenshot(…, colorScheme: 'd
208
208
  **Renkler hex olmalı.** CSS `rebeccapurple`'ı çizer ama studio'nun renk alanları hex bekler
209
209
  ve vurgu üstündeki metin rengi (`--ng-text-on-accent`) accent'i karakter karakter okuyup
210
210
  kontrast hesapladığı için tanımadığı değerde beyaza düşer (THEME-005).
211
+
212
+ ## Tasarım dosyasından tema (Figma) — tek geçiş
213
+
214
+ Bir Figma dosyası **bir kez** okunur, makine-okur bir **anlık görüntüye** alınır ve tema o
215
+ dosyadan türetilir. Sonrası Figma'sızdır: sayfa kurma, preset bağlama ve koyu mod anlık görüntü +
216
+ `theme_get` ile yürür. Ölçüm (2026-09-16): ekran frame'lerine atılan altı `get_design_context`
217
+ çağrısı yaklaşık **bir milyon karakter** React kodu getirdi; kullanılan yalnız renk, boşluk, yazı
218
+ tipi ve düzendi. O bilgi Figma'nın ucuz tool'larından çıkar.
219
+
220
+ **Bütçe: tasarım dosyası başına ≤ 8 Figma çağrısı, bağlama giren ≤ 150 bin karakter.**
221
+ İş tanımına frame URL'lerini yazdırın — sayfa düzeyinde ağaç dökümü tek başına 1 MB'tır.
222
+
223
+ | Adım | Figma çağrısı | Bütçe | Anlık görüntüye ne girer |
224
+ |---|---|---|---|
225
+ | 1 | `get_metadata(fileKey)` — `nodeId` YOK | 1 çağrı, < 2 K | `source.fileName`, sayfa id'si |
226
+ | 2 | **Koşullu:** insan frame URL'si vermediyse `get_metadata(fileKey, nodeId: <sayfa>)`; yalnız üst seviye frame'lerin ad + id'si okunur | 1 çağrı; büyükse harness dosyaya alır | `screens[]` (`role`'ü siz atarsınız: shell · list · form · detail · auth · other) |
227
+ | 3 | `get_variable_defs(fileKey, nodeId: <kütüphane/Components frame'i, yoksa ana ekran>)`; koyu mod frame'i varsa ona da. **Sayfa id'si (`0:1`) çalışmaz** — "select a layer" hatası; her zaman frame id verin. Farklı frame'ler farklı token dili taşıyabilir (ölçüldü: `Fonts/*` ve `Light/Basic/*` aynı dosyada) — iki temsilci frame yeter | 1–2 çağrı, 0,5–10 K | `variables`, `variablesDark` — **olduğu gibi**, ad yeniden yazılmaz; `Font(family: …, size: …, weight: …)` değerleri metin stili olarak okunur |
228
+ | 4 | `get_screenshot(fileKey, nodeId, maxDimension: 800)` — liste, form, detay için birer | ≤ 3 çağrı; varsayılan URL kipi, `curl -o` ile diske | `screens[].screenshot` (yerel yol), `layout.notes` (gözlem: ray genişliği, ızgara, kart/pill dili) |
229
+ | 5 | `get_design_context(fileKey, nodeId: <ATOMİK bileşen>, excludeScreenshot: true)` — button, badge, input; **ekran frame'ine ASLA** | ≤ 2 çağrı | `components` (ölçüler), `textStyles`, `effects` |
230
+
231
+ Tip ölçeği için sıra: (a) `variables` içinde `font/size/h1` gibi adlar, (b) kütüphanede bir
232
+ "Typography" bileşeni varsa 5. adımın bir çağrısı ona, (c) yoksa alan boş kalır ve tema önerisi
233
+ bunu `gaps` ile söyler — ekran frame'i bunun için de açılmaz.
234
+
235
+ **Anlık görüntü sözleşmesi** (`design-snapshot` altın örneği; her seviyede strict, tanınmayan
236
+ anahtar reddedilir):
237
+
238
+ | Alan | Ne |
239
+ |---|---|
240
+ | `source` | `kind` (`figma` \| `manual`), `fileKey`, `fileName`, `nodeIds`, `fetchedAt`, ölçüm için `calls` ve `characters` |
241
+ | `variables` · `variablesDark` | `get_variable_defs` çıktısı: `{ 'Primary': '#4787FA', 'spacing/md': 16 }`; koyu küme ayrı |
242
+ | `textStyles[]` | `name`, isteğe bağlı `role` (h1…small), `fontFamily`, `fontSize` (px), `fontWeight`, `lineHeight` |
243
+ | `effects[]` | gölge: `name`, offset/blur/spread, `color`, `opacity` (Figma'nın 0–1'i) |
244
+ | `layout` | `desktopWidth`, `mobileWidth`, `gutter`, `sidebarWidth`, `notes[]` — temaya gitmez, sayfa fazı için saklanır |
245
+ | `components` | `'button/primary'` → `background`, `color`, `border`, `borderRadius`, `paddingX/Y`, `height`, `fontSize`, `fontWeight`, `shadow` |
246
+ | `screens[]` | `nodeId`, `name`, `role`, `screenshot` (yerel yol), `notes[]` |
247
+ | `hints` | Figma adı → tema yolu (`accent.muted`, `colors.blue.500`) ya da `ignore` — **sizin kararınız, kalıcı** |
248
+
249
+ Dosya koşu klasöründe yaşar: `zeyrek-mcp/docs/<koşu>/design/<fileKey>.snapshot.json`. Ekran
250
+ görüntüleri repoya girmez. Gateway bu dosyayı hatırlamaz (HTTP taşımasında her çağrı yeni
251
+ sunucudur); ikinci oturum dosyayı okur, Figma'ya dönmez.
252
+
253
+ **Eksik değer için Figma'ya dönülmez.** Öneri bir token'ı eşleyemediyse sebebini `unmapped`'da
254
+ söyler; kararı `hints`'e yazıp yeniden çevirirsiniz. Değer dosyada hiç yoksa (`gaps`) ekran
255
+ görüntüsünden ya da insandan alıp anlık görüntüye elle eklersiniz (`source.kind` `figma` kalır,
256
+ `notes` söyler). Koyu palet aydınlıktan türetilmez: `variablesDark` yoksa `paletteDark` boşluktur
257
+ (THEME-009 uyarır).
258
+
259
+ Akış: anlık görüntüyü yaz → `theme_discover({ snapshot })` → `unmapped`/`gaps`'e bak, `hints` yaz →
260
+ `theme_discover` yeniden (`project` verirseniz mevcut temaya göre `cssVarsChanged` önizlemesi de
261
+ gelir) → `theme_update({ theme, dryRun: true })` → `theme_update({ theme })` →
262
+ `theme_get(view:'resolved')` → `page_screenshot` (aydınlık + `colorScheme:'dark'`). Bu rehberin
263
+ §Doğrulama sırası aynen geçerlidir.
264
+
265
+ `theme_discover` yazmaz ve tahmin etmez: öneri `theme_update` şemasındadır, `mapping` her Figma
266
+ adının hangi yola hangi kuralla (`role` · `ramp` · `hint` · `textStyle` · `variable` · `effect` ·
267
+ `component` · `component→var`) gittiğini söyler; yalnız SAPAN değer yazılır (platform varsayılanına
268
+ eşit taban ve ±0,05 içindeki oran öneriye girmez). Tek istisna `typeScale.p: 1` — platformda p
269
+ tabanın 0,925'idir, Figma gövdesi tabanın kendisi olduğu için yazılmazsa 15 px gövde 13,9 px
270
+ çizilir. Grup kelimesinin yanında tanınmayan bir niteleyici (`bg/error`, `text/on-primary`,
271
+ `button/primary`) varsayılana DÜŞMEZ, `unmapped` olur — kararı `hints` ile verirsiniz. Bileşen
272
+ ölçüleri adlı preset olur ve eşlenmiş renkler `var(--ng-*)` ile bağlanır (preset temayı izler);
273
+ `componentPresets` üretilmez. Gateway'in düzelttiği değerler (hex büyük harf, `rgba` → hex, gölge
274
+ `opacity` ×100, yazı tipi yazımı) yanıtta `gatewayNormalized` ile listelenir.
275
+
276
+ Bileşen adının ilk kelimesi katalogdaki bir TİP ise (`button/primary` → `button`) aynı ölçüler o
277
+ tipin **bileşen preseti** (`componentPresets.button.versions.V1.props.style`) olarak da önerilir:
278
+ adlı preset mevcut düğümlere bağlanır, bileşen preseti yeni düğümlere başlangıç olur. `input` katalog
279
+ tipi değildir ve `textinput` diye düzeltilmez — adı katalog tipiyle başlatın (`component_list`).
280
+ Koyu palet varsayılan olarak türetilmez; `theme_discover({ snapshot, deriveDark: true })` Figma'da
281
+ koyu küme YOKSA accent/status değerlerinden deterministik bir başlangıç noktası üretir ve her değeri
282
+ `via: 'derived'` ile işaretler — marka kararı değil, koyu kareyi görüp düzeltmek sizin işiniz.
283
+
284
+ **Ekranlar için `page_discover`.** Anlık görüntüdeki bir ekranın rolünden (list · form · detail ·
285
+ auth · shell) altın örnek arketipini seçip `page_create` + `page_schema_patch` iskeletini döner;
286
+ `theme_discover`'ın ürettiği adlı preset tek adaysa bileşene `props.style.$preset` olarak bağlanır.
287
+ Piksel kopyası değildir: `caveats` neyin doldurulacağını, `next` çağrı sırasını söyler.
288
+
289
+ `hints` biçimleri: çıplak `accent.hover` **iki kümeye de** uygulanır (Figma modları aynı adı
290
+ aydınlık ve koyu değerle verir — `variables`'taki `palette`'e, `variablesDark`'taki
291
+ `paletteDark`'a gider); `palette.accent.hover` / `paletteDark.accent.hover` yalnız kendi kümesinde;
292
+ `colors.blue.500` rampaya (yalnız aydınlık); `ignore` hiç eşleme. Anlık görüntüde olmayan bir ada
293
+ hint yazmak hatadır — Figma'da ad değiştiyse hint de değişir.
@@ -1,7 +1,7 @@
1
1
  import { fetchHost, flowPath, hostParam, unwrap } from './screens.js';
2
2
  import { guid, projectParam, environmentParam, readOnly, writes, updates, destructive, microOps } from './common.js';
3
3
  import { evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/policy.js';
4
- import { normalizeFlowSteps, targetlessSetterWarnings, unknownSetterTargetWarnings, withNormalization } from '../knowledge/normalize.js';
4
+ import { flowBindings, normalizeFlowSteps, targetlessSetterWarnings, unknownSetterTargetWarnings, withNormalization } from '../knowledge/normalize.js';
5
5
  import { flowBodyWarnings } from '../knowledge/coverage.js';
6
6
  import { PlatformApiError } from '../platform/errors.js';
7
7
  import { handle } from '../platform/responses.js';
@@ -50,6 +50,90 @@ function flowSummary(flow) {
50
50
  function hostNodes(record) {
51
51
  return record.schema ?? record.views ?? [];
52
52
  }
53
+ const BINDINGS_UNAVAILABLE = 'Barındırıcı kaydı okunamadığı için olay bağları hesaplanamadı; akış listesi yine de günceldir.';
54
+ /**
55
+ * Akış listesine bağları ekler. `danglingBindings` ters yönü söyler: şemada id'si geçen ama
56
+ * barındırıcıda karşılığı olmayan akış — olay tetiklenir, hiçbir şey koşmaz.
57
+ */
58
+ function flowListResult(flows, record) {
59
+ if (record === undefined)
60
+ return { flows: flows.map(flowSummary), bindingsNote: BINDINGS_UNAVAILABLE };
61
+ const bindings = flowBindings(hostNodes(record), record.events);
62
+ const known = new Set(flows.map(flow => String(flow.id).toLowerCase()));
63
+ const danglingBindings = [...bindings.entries()]
64
+ .filter(([flowId]) => !known.has(flowId))
65
+ .flatMap(([flowId, entries]) => entries.map(entry => ({ flowId, ...entry })));
66
+ return {
67
+ flows: flows.map(flow => ({ ...flowSummary(flow), bindings: bindings.get(String(flow.id).toLowerCase()) ?? [] })),
68
+ unboundFlowIds: flows.map(flow => String(flow.id)).filter(flowId => !bindings.has(flowId.toLowerCase())),
69
+ ...(danglingBindings.length > 0 ? { danglingBindings } : {})
70
+ };
71
+ }
72
+ 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).");
73
+ const upper = (value) => String(value ?? '').toUpperCase();
74
+ /**
75
+ * Barındırıcının akışları, önbelleksiz liste ucundan (`GET /pages|layouts/{id}/flows`). Layout ve
76
+ * sayfa tasarım kaydı önbellekten gelir ve zarfsız akış yazmasından sonra akışı hemen taşımaz;
77
+ * 2026-09-16 raporundaki kopyalar bu yüzden doğdu. Widget kaydı önbelleksizdir, oradan okunur.
78
+ * Layout ucu olmayan eski backend 404/405 verir: o zaman kayda düşülür.
79
+ */
80
+ async function listHostFlows(platform, host, project, environment, applicationId, hostId, hostRecord) {
81
+ if (host !== 'component') {
82
+ try {
83
+ const payload = await platform.tenant('GET', project, flowPath(host, applicationId, hostId), { environment });
84
+ if (!Array.isArray(payload?.data))
85
+ throw new PlatformApiError('akış listesi yanıtında data dizisi yok');
86
+ return payload.data;
87
+ }
88
+ catch (error) {
89
+ const status = error instanceof PlatformApiError ? error.status : undefined;
90
+ if (host === 'page' || (status !== 404 && status !== 405))
91
+ throw error;
92
+ }
93
+ }
94
+ const record = await hostRecord();
95
+ return Array.isArray(record.flows) ? record.flows : [];
96
+ }
97
+ /**
98
+ * FLOW-026 kapısı. Aynı adla ikinci akışın `"<ad> (1)"` olarak açılması platformun ürün kararıdır
99
+ * (`FlowService.MakeNameUniqueAsync`; Studio'da da sayfa/workflow/akış aynı kuralla adlanır) ve
100
+ * değişmez. MCP ajanı ise çağrıyı tekrar edebildiği için varsayılanı "yazma, mevcut id'yi söyle"dir.
101
+ * Aday araması platformda `LIKE` ile harfe duyarlıdır; gateway bilinçli olarak harf duyarsız
102
+ * karşılaştırır. Var-mı okuması başarısız olursa çağrı düşer — okuyamadığı listeyi boş sayan kontrol
103
+ * tam da zaman aşımında kopya üretirdi.
104
+ */
105
+ async function existingFlowMatch(platform, host, project, environment, applicationId, hostId, hostRecord, name, slug, flowId, ifExists) {
106
+ if (ifExists === 'duplicate')
107
+ return undefined;
108
+ let flows;
109
+ try {
110
+ flows = await listHostFlows(platform, host, project, environment, applicationId, hostId, async () => hostRecord);
111
+ }
112
+ catch (error) {
113
+ 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.`);
114
+ }
115
+ const matches = flows.filter(flow => upper(flow.name) === upper(name) || upper(flow.slug) === upper(slug));
116
+ const [match] = matches;
117
+ if (match === undefined)
118
+ return undefined;
119
+ const describe = (flow) => `${String(flow.id)} ('${String(flow.name)}', slug '${String(flow.slug)}')`;
120
+ const copyPattern = new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} \\(\\d+\\)$`, 'i');
121
+ const copies = flows.filter(flow => copyPattern.test(String(flow.name ?? '')));
122
+ const copyNote = copies.length > 0
123
+ ? ` 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.`
124
+ : '';
125
+ if (ifExists === 'error') {
126
+ const where = host === 'page' ? 'sayfada' : host === 'layout' ? "layout'ta" : "widget'ta";
127
+ 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);
128
+ }
129
+ if (matches.length > 1) {
130
+ 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);
131
+ }
132
+ if (flowId !== undefined && flowId.toLowerCase() !== String(match.id).toLowerCase()) {
133
+ 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);
134
+ }
135
+ return match;
136
+ }
53
137
  async function requiredParameterWarnings(platform, project, environment, applicationId, steps) {
54
138
  const warnings = [];
55
139
  const microserviceSteps = steps
@@ -226,32 +310,41 @@ export function registerFlowTools(server, platform) {
226
310
  const config = platform.config;
227
311
  server.registerTool('flow_list', {
228
312
  title: 'Akışları listele',
229
- description: "Barındırıcının olay akışlarını listeler. Sayfada ayrı bir liste ucu vardır; layout ve komponentte akışlar kaydın kendi gövdesinden okunur.",
313
+ description: "Barındırıcının olay akışlarını, her birine bağlı olaylarla (bindings: [{ target: elementId | 'page', event }]) listeler. unboundFlowIds hiçbir olaya bağlı olmayan (sahipsiz) akışlardır; danglingBindings şemada id'si geçen ama barındırıcıda olmayan akışlardır. Sayfa ve layout akışları önbelleksiz liste ucundan, widget akışları kaydın kendi gövdesinden okunur; bağlar barındırıcının şemasından hesaplanır.",
230
314
  inputSchema: { project: projectParam, environment: environmentParam, host: hostParam, applicationId: guid, hostId: guid },
231
315
  annotations: readOnly
232
316
  }, handle(config, async ({ project, environment, host, applicationId, hostId }) => {
233
- if (host === 'page') {
234
- const payload = await platform.tenant('GET', project, flowPath('page', applicationId, hostId), { environment });
235
- return { flows: (payload?.data ?? []).map(flowSummary) };
317
+ if (host === 'component') {
318
+ const record = await fetchHost(platform, host, project, environment, applicationId, hostId);
319
+ return flowListResult(Array.isArray(record.flows) ? record.flows : [], record);
236
320
  }
237
- const record = await fetchHost(platform, host, project, environment, applicationId, hostId);
238
- return { flows: (Array.isArray(record.flows) ? record.flows : []).map(flowSummary) };
321
+ let record;
322
+ const readRecord = async () => (record ??= await fetchHost(platform, host, project, environment, applicationId, hostId));
323
+ const flows = await listHostFlows(platform, host, project, environment, applicationId, hostId, readRecord);
324
+ return flowListResult(flows, record ?? await readRecord().catch(() => undefined));
239
325
  }));
240
326
  server.registerTool('flow_get', {
241
327
  title: 'Akış getir',
242
- description: "Tek akışı adımlarıyla getirir; yeni akış yazarken en iyi şablon budur. Hedefi silinmiş (element'siz) setter iteratee'si varsa yanıttaki flowWarnings söyler — o adım koşar ama hiçbir şeye yazmaz.",
328
+ description: "Tek akışı adımlarıyla getirir; yeni akış yazarken en iyi şablon budur. bindings bu akışa bağlı olaylardır ([{ target: elementId | 'page', event }]; boşsa akış sahipsizdir). Hedefi silinmiş (element'siz) setter iteratee'si varsa yanıttaki flowWarnings söyler — o adım koşar ama hiçbir şeye yazmaz.",
243
329
  inputSchema: { project: projectParam, environment: environmentParam, host: hostParam, applicationId: guid, hostId: guid, flowId: flowIdParam },
244
330
  annotations: readOnly
245
331
  }, handle(config, async ({ project, environment, host, applicationId, hostId, flowId }) => {
246
332
  const response = await platform.tenant('GET', project, flowPath(host, applicationId, hostId, flowId), { environment });
333
+ const record = await fetchHost(platform, host, project, environment, applicationId, hostId).catch(() => undefined);
247
334
  const warnings = targetlessSetterWarnings(response);
248
- if (warnings.length === 0 || !response || typeof response !== 'object' || Array.isArray(response))
335
+ if (!response || typeof response !== 'object' || Array.isArray(response))
249
336
  return response;
250
- return { ...response, flowWarnings: warnings };
337
+ return {
338
+ ...response,
339
+ ...(record === undefined
340
+ ? { bindingsNote: BINDINGS_UNAVAILABLE }
341
+ : { bindings: flowBindings(hostNodes(record), record.events).get(flowId.toLowerCase()) ?? [] }),
342
+ ...(warnings.length > 0 ? { flowWarnings: warnings } : {})
343
+ };
251
344
  }));
252
345
  server.registerTool('flow_create', {
253
346
  title: 'Akış oluştur',
254
- description: "Sayfa/layout/komponent için olay akışı oluşturur. İstek yalnız akışı yazar, barındırıcının şemasına dokunmaz; gateway barındırıcıyı yalnız setter hedeflerini denetlemek için okur (FLOW-024). Akışı bir komponent olayına bağlamak ayrı adımdır ve akış kaydedildikten SONRA yapılır: set_event {target, event, eventValue: <flow id>} — sayfada page_schema_patch, layout'ta layout_schema_patch, widget'ta component_schema_patch.",
347
+ description: "Sayfa/layout/komponent için olay akışı oluşturur. Aynı ad ya da slug'da akış varsa varsayılan olarak YAZMAZ ve mevcut akışın id'sini döner (FLOW-026 — gönderilseydi platform ürün kuralı gereği '<ad> (1)' adıyla ikinci bir akış açardı); tekrar çalıştırılabilir yazma için ifExists:'update' verin, yanıttaki outcome 'created' | 'updated' olur. Mevcut akışlar okunamazsa da yazmaz. İstek yalnız akışı yazar, barındırıcının şemasına dokunmaz; gateway barındırıcıyı setter hedeflerini denetlemek (FLOW-024), mevcut akışları ise önbelleksiz liste ucundan okur. Akışı bir komponent olayına bağlamak ayrı adımdır ve akış kaydedildikten SONRA yapılır: set_event {target, event, eventValue: <flow id>} — sayfada page_schema_patch, layout'ta layout_schema_patch, widget'ta component_schema_patch. Hangi olayların bağlı olduğunu flow_list / flow_get bindings alanı söyler.",
255
348
  inputSchema: {
256
349
  project: projectParam,
257
350
  environment: environmentParam,
@@ -263,12 +356,13 @@ export function registerFlowTools(server, platform) {
263
356
  steps: stepsParam,
264
357
  errorSteps: stepsParam.optional(),
265
358
  flowId: guid.optional().describe('Verilmezse platform üretir; şemaya önceden bağlamak için kendiniz üretebilirsiniz'),
359
+ ifExists: ifExistsParam,
266
360
  policyOverride: policyOverrideParam
267
361
  },
268
362
  annotations: writes
269
- }, handle(config, async ({ project, environment, host, applicationId, hostId, name, slug, steps, errorSteps, flowId, policyOverride }) => {
363
+ }, handle(config, async ({ project, environment, host, applicationId, hostId, name, slug, steps, errorSteps, flowId, ifExists, policyOverride }) => {
270
364
  const hostRecord = await fetchHost(platform, host, project, environment, applicationId, hostId);
271
- const resolvedFlowId = flowId ?? randomUUID();
365
+ const resolvedFlowId = (flowId ?? randomUUID()).toLowerCase();
272
366
  const flow = {
273
367
  id: resolvedFlowId,
274
368
  name,
@@ -279,6 +373,8 @@ export function registerFlowTools(server, platform) {
279
373
  };
280
374
  const normalized = [...normalizeFlowSteps(flow.steps), ...normalizeFlowSteps(flow.errorSteps)];
281
375
  const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'flow', flow, { operation: 'create' }, policyOverride));
376
+ const existing = await existingFlowMatch(platform, host, project, environment, applicationId, hostId, hostRecord, name, slug, flowId, ifExists);
377
+ const writtenFlowId = existing === undefined ? resolvedFlowId : String(existing.id);
282
378
  const parameterWarnings = [
283
379
  ...await requiredParameterWarnings(platform, project, environment, applicationId, steps),
284
380
  ...await functionWarnings(platform, host, project, environment, applicationId, hostId, steps),
@@ -287,15 +383,21 @@ export function registerFlowTools(server, platform) {
287
383
  ];
288
384
  // Gövde yalnız akıştır (designer-partial-update K1): barındırıcı zarfı platformda hâlâ isteğe bağlı
289
385
  // ama gönderilirse okunan tam kaydı yeniden yazar, açık Studio designer'ının kayıtlı tabanını
290
- // eskitir. Kayıt yalnız FLOW-024 hedef araması için okunur.
291
- await platform.tenant('POST', project, flowPath(host, applicationId, hostId), { environment, body: { flow } });
386
+ // eskitir. Kayıt FLOW-024 hedef araması ve FLOW-026 var-mı kontrolü için okunur.
387
+ if (existing === undefined) {
388
+ await platform.tenant('POST', project, flowPath(host, applicationId, hostId), { environment, body: { flow } });
389
+ }
390
+ else {
391
+ await platform.tenant('PUT', project, flowPath(host, applicationId, hostId, writtenFlowId), { environment, body: { flow: { ...flow, id: writtenFlowId } } });
392
+ }
292
393
  // Diff YALNIZ çağıranın yazdığı alanları kapsar; `id` ve barındırıcı bağı (`pageId` vb.)
293
394
  // gövdeye gateway koyar, kullanıcının düzeltebileceği bir şey değildir.
294
- const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, resolvedFlowId, { name: flow.name, slug: flow.slug, steps: flow.steps, errorSteps: flow.errorSteps });
395
+ const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, writtenFlowId, { name: flow.name, slug: flow.slug, steps: flow.steps, errorSteps: flow.errorSteps });
295
396
  return attachFindings(withWriteDiff(withNormalization({
296
397
  host,
297
398
  hostId,
298
- flow: { id: resolvedFlowId, name, slug, steps: steps.length },
399
+ outcome: existing === undefined ? 'created' : 'updated',
400
+ flow: { id: writtenFlowId, name, slug, steps: steps.length },
299
401
  ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
300
402
  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."
301
403
  }, normalized), drops), findings);
@@ -19,6 +19,8 @@ import { resolveIdentityToken } from '../platform/tokenVault.js';
19
19
  import { mkdir, writeFile } from 'node:fs/promises';
20
20
  import { dirname, resolve } from 'node:path';
21
21
  import { setPropTypeErrors } from '../knowledge/propTypes.js';
22
+ import { designSnapshotInput } from '../knowledge/designSnapshot.js';
23
+ import { discoverPage } from '../knowledge/pageDiscover.js';
22
24
  import { z } from 'zod';
23
25
  /**
24
26
  * Runtime paketindeki `appFunctions`/`pageFunctions` (UiFunctionRuntimeDto: id, name, parameters,
@@ -800,6 +802,20 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
800
802
  ...flexIndexOrderNote(ordering.moved)
801
803
  ]), opDrops);
802
804
  }));
805
+ server.registerTool('page_discover', {
806
+ title: 'Tasarım ekranından sayfa iskeleti öner',
807
+ description: "Tasarım anlık görüntüsündeki bir EKRANIN rolünden (list · form · detail · auth · shell — sözleşme platform_guide('theme') §Tasarım dosyasından tema) altın örnek arketipini seçip page_create + page_schema_patch iskeletini döner: pageCreateArgs (ad/slug ekrandan), schemaPatchOps, flows (örneğin akış gövdeleri), presetBindings (theme_discover'ın aynı anlık görüntüden ürettiği adlı preset TEK adaysa props.style.$preset olarak bağlanır), notes (yerleşim + ekran notları), caveats (yer tutucular, doldurulacak alanlar), next (çağrı sırası). shell rolü layout_create iskeleti döner. PİKSEL KOPYASI DEĞİLDİR: iskelet örneğin domainindedir, ajan ekran notlarına göre tamamlar. YAZMAZ, platforma gitmez, tahmin yapmaz.",
808
+ inputSchema: {
809
+ snapshot: designSnapshotInput.describe('Tasarım anlık görüntüsü (screens[] dolu olmalı)'),
810
+ screen: z.string().min(1).describe("screens[] içindeki ekranın nodeId'si ('92:4339') ya da adı ('Deals')"),
811
+ layoutId: guid.optional().describe('Sayfanın kabuğu (layout_list); verilmezse pageCreateArgs kabuksuz döner ve caveats söyler'),
812
+ middlewareIds: z.array(guid).optional().describe('Korumalı sayfa için auth middleware id listesi (middleware_list); verilmezse örnekteki yer tutucu düşer ve caveats söyler')
813
+ },
814
+ annotations: readOnly
815
+ }, handle(config, async ({ snapshot, screen, layoutId, middlewareIds }) => discoverPage(snapshot, screen, {
816
+ ...(layoutId === undefined ? {} : { layoutId }),
817
+ ...(middlewareIds === undefined ? {} : { middlewareIds })
818
+ })));
803
819
  server.registerTool('page_create', {
804
820
  title: 'Sayfa oluştur',
805
821
  description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (canlı canvas ve hedefli düzeltme orada). Şemayı burada gönderirseniz 2026-09-08'den beri AYNI doğrulama duvarından geçer: tanınmayan prop adı ve geçersiz enum değeri 422 ile reddedilir ve sayfa HİÇ açılmaz. roleIds için role_list, layoutId için layout_list kullanın. Yanıttaki schema VARSAYILAN OLARAK iskelettir (çözülmüş layout düğümleri dâhil id/type/children'a iner) — tam gövde için view:'full'; gönderdiğiniz bir alan kaydedilmediyse silentlyDropped söyler.",
@@ -2,6 +2,8 @@ import { guid, projectParam, readOnly, updates } from './common.js';
2
2
  import { diffCssVars, mergeRadiusScale, mergeSpacingScale, mergeTypeScale, resolveTheme } from '../knowledge/themeResolve.js';
3
3
  import { diffThemePaths, mergeTheme, normalizeTheme, readProjectTheme, summarizeTheme, themeInput, unsetThemePaths } from '../knowledge/themeSchema.js';
4
4
  import { componentPresetTypeWarnings, themeInputWarnings, unsetWarnings } from '../knowledge/themeRules.js';
5
+ import { designSnapshotInput } from '../knowledge/designSnapshot.js';
6
+ import { discoverTheme } from '../knowledge/themeDiscover.js';
5
7
  import { attachFindings, enforcePolicy, evaluatePolicy, applyPolicyMode } from '../knowledge/policy.js';
6
8
  import { projectUpdateForm } from './projects.js';
7
9
  import { withNormalization } from '../knowledge/normalize.js';
@@ -90,6 +92,56 @@ export function registerThemeTools(server, platform) {
90
92
  note: "Değerlerin ekrana indiği hâl için view:'resolved'; yazmak için theme_update (kısmi günceller, lint'ten geçirir, hangi CSS değişkeninin değiştiğini söyler). Adlı bir preset'i düğüme bağlamak: style.$preset — platform_guide('theme')."
91
93
  };
92
94
  }));
95
+ server.registerTool('theme_discover', {
96
+ title: 'Tasarım anlık görüntüsünden tema öner',
97
+ description: "Bir tasarım anlık görüntüsünü (Figma değişkenleri, metin stilleri, gölgeler, bileşen ölçüleri — sözleşme platform_guide('theme') §Tasarım dosyasından tema, altın örnek design-snapshot) theme_update şemasında bir tema önerisine çevirir ve HESABINI verir: mapping (hangi ad hangi yola, hangi kuralla), unmapped (neden gitmedi), gaps (kaynağı olmayan alan), lint uyarıları; project verilirse mevcut temaya göre changed + cssVarsChanged önizlemesi. YAZMAZ — yazmak için theme_update({ theme }), önce dryRun. Eşleme ad temelli ve deterministiktir, tahmin yapmaz: eşlenmeyen token için kararı anlık görüntünün hints alanına yazarsınız ve karar dosyada kalır — Figma'ya ikinci kez gidilmez. Yalnız sapan değer yazılır: platform varsayılanına eşit taban ve ±0,05 içindeki oran öneriye girmez.",
98
+ inputSchema: {
99
+ snapshot: designSnapshotInput.describe("Tasarım anlık görüntüsü — sözleşme platform_guide('theme') §Tasarım dosyasından tema; her seviyede strict, tanınmayan anahtar reddedilir. variables get_variable_defs çıktısının kendisidir; hints: Figma adı → tema yolu (accent.muted, paletteDark.accent.primary, colors.blue.500) ya da 'ignore'"),
100
+ project: projectParam.describe("Yalnız ÖNİZLEME için: verilirse projenin mevcut teması okunur (GET) ve öneri ona derin birleştirilerek changed + cssVarsChanged hesaplanır. Verilmezse platforma HİÇ gidilmez — DefaultProject burada devreye girmez"),
101
+ deriveDark: z.boolean().optional().describe("true: anlık görüntüde koyu küme (variablesDark) YOKSA paletteDark aydınlık accent/status değerlerinden deterministik karışımla türetilir ve her değer mapping.via:'derived' ile işaretlenir — marka kararı değil başlangıç noktası. variablesDark varsa bayrak yok sayılır. Varsayılan: türetme yok (gaps: paletteDark, THEME-009)")
102
+ },
103
+ annotations: readOnly
104
+ }, handle(config, async ({ snapshot, project, deriveDark }) => {
105
+ const discovered = discoverTheme(snapshot, { deriveDark: deriveDark === true });
106
+ const proposal = normalizeTheme(discovered.theme);
107
+ const fixes = [...discovered.normalized, ...proposal.fixes];
108
+ let preview;
109
+ let merged = proposal.theme;
110
+ if (project !== undefined) {
111
+ const { project: projectInfo, theme: base } = await loadProject(platform, project);
112
+ const current = base ?? {};
113
+ merged = normalizeTheme(mergeTheme(current, proposal.theme)).theme;
114
+ preview = {
115
+ project: projectInfo,
116
+ changed: diffThemePaths(current, merged),
117
+ cssVarsChanged: diffCssVars(resolveTheme(current).cssVars, resolveTheme(merged).cssVars),
118
+ note: 'theme_update({ theme, dryRun: true }) aynı sonucu verir; bu önizleme yalnız GET ile hesaplandı, hiçbir şey yazılmadı.'
119
+ };
120
+ }
121
+ // Uyarılar YAZMA anındaki kurallarla aynı motordan (THEME-005/008/009 …): ajan öneriyi
122
+ // theme_update'e taşımadan ne uyarı alacağını burada görür. Block kuralları da listelenir ama
123
+ // enforce edilmez — bu tool yazmıyor; yazma anında theme_update durdurur. Operasyon 'update'
124
+ // çünkü tema kuralları yalnız ona kayıtlı (`appliesTo: ['update']`); 'preview' ile hiçbiri
125
+ // değerlendirilmiyordu (inceleme B-7). Sorulan şey aynı: "bu kayıt yazılsa geçer mi".
126
+ const findings = config.policyMode === 'off'
127
+ ? []
128
+ : applyPolicyMode(config, [
129
+ ...evaluatePolicy(config, policyRules, 'theme', merged, { operation: 'update' }),
130
+ ...themeInputWarnings(proposal.theme, merged)
131
+ ]);
132
+ const empty = Object.keys(proposal.theme).length === 0;
133
+ return attachFindings(withNormalization({
134
+ source: snapshot.source,
135
+ theme: proposal.theme,
136
+ mapping: discovered.mapping,
137
+ unmapped: discovered.unmapped,
138
+ gaps: discovered.gaps,
139
+ ...(preview === undefined ? {} : { preview }),
140
+ note: empty
141
+ ? 'Anlık görüntüde eşlenebilir token yok — unmapped (sebep) ve gaps (kaynağı olmayan alan) listelerine bakın; kararı hints ile verin.'
142
+ : "Yazmak için theme_update({ theme }) — önce dryRun: true. unmapped kararlarını anlık görüntünün hints alanına yazın ve dosyayı koşu klasöründe bırakın; ikinci koşu Figma'ya dönmez. gaps'teki alanlar uydurulmadı: kaynağı ekleyin ya da boş bırakın."
143
+ }, fixes), findings);
144
+ }));
93
145
  server.registerTool('theme_update', {
94
146
  title: 'Proje temasını güncelle',
95
147
  description: "Temayı KISMİ günceller: kaydı okur, verdiğiniz alanları derin birleştirir, normalize eder, lint'ten geçirir ve yazar — bir rengi değiştirmek preset'leri ve bileşen preset'lerini düşürmez. Yanıt hangi yolların değiştiğini (changed) ve hangi CSS değişkeninin hangi değerden hangi değere gittiğini (cssVarsChanged) söyler; dryRun ile yazmadan aynı yanıtı alırsınız. DİKKAT: tema PROJE kaydıdır — değişiklik BÜTÜN ortamların runtime'ına iner (production dâhil) ve Studio tema paneli açık ve kaydedilmemişse panelin sonraki Save'i bu yazmayı ezer.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appaflytech/zeyrek-ai-mcp",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "description": "Zeyrek (NgApps) platform APIs as MCP tools — data, logic and Studio design surface in one gateway",
6
6
  "license": "UNLICENSED",
@@ -54,4 +54,4 @@
54
54
  "typescript": "^5.7.2",
55
55
  "vitest": "^3.0.0"
56
56
  }
57
- }
57
+ }