@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.
- package/README.md +11 -6
- package/dist/knowledge/designSnapshot.js +77 -0
- package/dist/knowledge/normalize.js +47 -0
- package/dist/knowledge/outlines.js +24 -7
- package/dist/knowledge/pageDiscover.js +151 -0
- package/dist/knowledge/themeDiscover.js +884 -0
- package/dist/knowledge/themeResolve.js +4 -2
- package/dist/resources/examples/design-snapshot.json +157 -0
- package/dist/resources/guides/theme.md +84 -1
- package/dist/tools/flows.js +119 -17
- package/dist/tools/pages.js +16 -0
- package/dist/tools/themes.js +52 -0
- package/package.json +2 -2
- package/dist/tools/globalFunctions.js +0 -63
|
@@ -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
|
-
|
|
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'
|
|
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.
|
package/dist/tools/flows.js
CHANGED
|
@@ -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
|
|
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 === '
|
|
234
|
-
const
|
|
235
|
-
return
|
|
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
|
-
|
|
238
|
-
|
|
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 (
|
|
335
|
+
if (!response || typeof response !== 'object' || Array.isArray(response))
|
|
249
336
|
return response;
|
|
250
|
-
return {
|
|
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ı
|
|
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
|
|
291
|
-
|
|
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,
|
|
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
|
-
|
|
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);
|
package/dist/tools/pages.js
CHANGED
|
@@ -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.",
|
package/dist/tools/themes.js
CHANGED
|
@@ -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.
|
|
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
|
+
}
|