@appaflytech/zeyrek-ai-mcp 0.0.8 → 0.0.9
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 +27 -21
- package/dist/config.js +2 -0
- package/dist/knowledge/componentCatalog.js +122 -27
- package/dist/knowledge/componentEvents.js +35 -4
- package/dist/knowledge/errorPath.js +1 -1
- package/dist/knowledge/normalize.js +15 -0
- package/dist/knowledge/rules.js +3 -1
- package/dist/knowledge/styleResolve.js +46 -4
- package/dist/knowledge/themeResolve.js +936 -0
- package/dist/knowledge/themeRules.js +221 -0
- package/dist/knowledge/themeSchema.js +335 -0
- package/dist/platform/domSummary.js +144 -0
- package/dist/platform/screenshot.js +414 -23
- package/dist/platform/screenshotMemory.js +55 -0
- package/dist/resources/examples/layout-app-shell.json +1 -1
- package/dist/resources/examples/theme-brand.json +162 -0
- package/dist/resources/guides/component.md +7 -1
- package/dist/resources/guides/getting-started.md +4 -0
- package/dist/resources/guides/layout.md +30 -26
- package/dist/resources/guides/microservice.md +55 -1
- package/dist/resources/guides/page.md +87 -44
- package/dist/resources/guides/theme.md +210 -0
- package/dist/resources/guides/translation.md +6 -4
- package/dist/resources/guides/workflow.md +91 -6
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/server.js +52 -43
- package/dist/tools/components.js +8 -0
- package/dist/tools/designSchemaPatch.js +152 -0
- package/dist/tools/flows.js +15 -18
- package/dist/tools/layouts.js +4 -4
- package/dist/tools/microservices.js +5 -5
- package/dist/tools/pages.js +196 -323
- package/dist/tools/projects.js +66 -9
- package/dist/tools/schemaOps.js +280 -0
- package/dist/tools/screenWrite.js +2 -1
- package/dist/tools/screens.js +0 -7
- package/dist/tools/themes.js +163 -0
- package/dist/tools/workflows.js +1 -1
- package/package.json +1 -1
package/dist/tools/projects.js
CHANGED
|
@@ -8,6 +8,7 @@ import { z } from 'zod';
|
|
|
8
8
|
import { evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/policy.js';
|
|
9
9
|
import { policyRules } from '../knowledge/rules.js';
|
|
10
10
|
import { constraintSummary } from '../knowledge/backendConstraints.js';
|
|
11
|
+
import { readProjectTheme } from '../knowledge/themeSchema.js';
|
|
11
12
|
import { readProjectLanguages, runtimeResolvesTranslations, resolveSeedLanguages, SEED_LANGUAGES, SEED_DEFAULT_LANGUAGE } from '../knowledge/languages.js';
|
|
12
13
|
const languageCode = z.string().regex(/^[a-z]{2}$/, 'ISO 639-1 iki harfli küçük dil kodu (tr, en, de …)');
|
|
13
14
|
function localizationFix(state) {
|
|
@@ -80,6 +81,45 @@ const templateOptionsInput = z.object({
|
|
|
80
81
|
templateId: guid,
|
|
81
82
|
templateReceiveUpdates: z.boolean().optional()
|
|
82
83
|
});
|
|
84
|
+
const themeInput = z.union([z.string(), z.record(z.unknown())]);
|
|
85
|
+
/**
|
|
86
|
+
* `Theme` form alanının değeri. Platform alanı JSON METNİ olarak alıyor
|
|
87
|
+
* (`UpdateOrganizationProjectCommand.Theme` bir `string`), ama ajan doğal olarak nesne yazıyor:
|
|
88
|
+
* nesne gateway'de serileştirilir. Metin verildiyse JSON olduğu İSTEK GİTMEDEN denetlenir —
|
|
89
|
+
* geçersiz metin platformda `ToJsonDocument()` sırasında patlar ve mesaj hangi alanın bozuk
|
|
90
|
+
* olduğunu söylemez.
|
|
91
|
+
*/
|
|
92
|
+
export function themeField(theme, tool) {
|
|
93
|
+
if (theme === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (typeof theme !== 'string')
|
|
96
|
+
return JSON.stringify(theme);
|
|
97
|
+
try {
|
|
98
|
+
JSON.parse(theme);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new PlatformApiError(`${tool}: 'theme' metni JSON olarak çözülemedi (${error.message}). Temayı nesne olarak verin — gateway serileştirir.`, 400);
|
|
102
|
+
}
|
|
103
|
+
return theme;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `PUT /projects/{id}` gövdesinin ortak üçlüsü.
|
|
107
|
+
*
|
|
108
|
+
* Uç bir REPLACE'tir ve eksik alan "koru" değil **SİL** demektir: `OrganizationProject.Override`
|
|
109
|
+
* `Theme = theme;` satırını koşulsuz çalıştırır (`OrganizationProject.cs:103`), `Updated` olayı da
|
|
110
|
+
* sonucu her aktif ortamın tenant kaydına kopyalar
|
|
111
|
+
* (`UpdateProjectWhenOrganizationProjectUpdatedEventHandler.cs:36`). Yani yalnız dil ayarını
|
|
112
|
+
* değiştiren bir çağrı markanın tamamını düşürüyordu ve yanıt 200 dönüyordu (S-1). Gövdeyi kuran
|
|
113
|
+
* tek yer burasıdır ki aynı uca yazan her tool aynı korumayı paylaşsın.
|
|
114
|
+
*/
|
|
115
|
+
export function projectUpdateForm(fields) {
|
|
116
|
+
const form = new URLSearchParams({ Name: fields.name });
|
|
117
|
+
if (fields.slug)
|
|
118
|
+
form.set('Slug', fields.slug);
|
|
119
|
+
if (fields.theme !== undefined)
|
|
120
|
+
form.set('Theme', fields.theme);
|
|
121
|
+
return form;
|
|
122
|
+
}
|
|
83
123
|
export function registerProjectTools(server, platform) {
|
|
84
124
|
const config = platform.config;
|
|
85
125
|
server.registerTool('project_list', {
|
|
@@ -105,7 +145,7 @@ export function registerProjectTools(server, platform) {
|
|
|
105
145
|
inputSchema: {
|
|
106
146
|
name: z.string().min(1),
|
|
107
147
|
slug: z.string().min(1).describe('Organizasyonda tekil; veritabanı adı zeyrek_<org>_<slug> bundan türer ve sonradan değiştirmek bütün runtime rotalarını kırar'),
|
|
108
|
-
theme:
|
|
148
|
+
theme: themeInput.optional().describe("Tema nesnesi (ya da JSON metni) — Studio tema panelinin yazdığı kayıtla aynı şekil: mode · palette · paletteDark · fontFamily · typeScale · shadow · presets · componentPresets. Verilmezse proje platformun varsayılan temasıyla (accent #0052CC, Inter 14px) doğar. Kurulmuş bir projenin temasını DEĞİŞTİRMEK için theme_update: kısmi günceller, şema/lint'ten geçirir ve hangi CSS değişkeninin değiştiğini söyler"),
|
|
109
149
|
languages: z.array(z.string().min(1)).min(1).optional().describe(`Proje dilleri (ISO 639-1; 'tr-TR' gibi bölgeli değer kabul edilir ve 'tr'ye indirgenir, indirgeme gatewayNormalized ile bildirilir). Verilmezse gateway [${SEED_LANGUAGES.join(', ')}] tohumlar — projenin dilsiz doğmasına ve çevirilerin sessizce çözülmemesine izin verilmez`),
|
|
110
150
|
defaultLanguage: z.string().min(1).optional().describe(`languages içinde olmalı (indirgenmiş hâliyle karşılaştırılır); yalnız defaultLanguage verirseniz languages onunla kurulur. İkisi de yoksa '${SEED_DEFAULT_LANGUAGE}'`),
|
|
111
151
|
application: applicationInput.optional(),
|
|
@@ -130,8 +170,9 @@ export function registerProjectTools(server, platform) {
|
|
|
130
170
|
}
|
|
131
171
|
const form = new URLSearchParams({ Name: args.name, Slug: args.slug });
|
|
132
172
|
const notes = [];
|
|
133
|
-
|
|
134
|
-
|
|
173
|
+
const theme = themeField(args.theme, 'project_create');
|
|
174
|
+
if (theme !== undefined)
|
|
175
|
+
form.set('Theme', theme);
|
|
135
176
|
if (args.application) {
|
|
136
177
|
form.set('Application.Slug', args.application.slug);
|
|
137
178
|
form.set('Application.Name', args.application.name);
|
|
@@ -171,13 +212,15 @@ export function registerProjectTools(server, platform) {
|
|
|
171
212
|
id: guid,
|
|
172
213
|
name: z.string().min(1).optional().describe('Verilmezse mevcut ad korunur — yalnız dil ayarını değiştirmek için adı tekrar göndermeniz gerekmez'),
|
|
173
214
|
slug: z.string().optional(),
|
|
174
|
-
theme:
|
|
215
|
+
theme: themeInput.optional().describe("Tema nesnesi (ya da JSON metni); verilen kayıt mevcut temanın YERİNE geçer — bir alanı değiştirmek için theme_update kullanın (kısmi birleştirir, lint'ten geçirir). VERİLMEZSE mevcut tema korunur — gateway projeden okuyup gövdeye geri koyar, çünkü platform Theme alanı gövdede yokken temayı siler ve silinen tema bütün ortamların runtime'ına iner (S-1). Temayı gerçekten temizlemek için theme: {} verin"),
|
|
175
216
|
languages: z.array(languageCode).min(1).optional().describe("ISO 639-1 iki harfli kod listesi (proje dilleri, platformun 184 kodluk sözlüğünden — PRJ-002). Runtime çevirileri YALNIZ bu kodlarla çözer; liste kurulmadan yazılan çeviriler 200 döner ama ekrana ham anahtar basılır (S-15). Yeni proje bu liste BOŞ doğar — çeviri kullanacaksanız burayı kurmak ilk adımdır"),
|
|
176
217
|
defaultLanguage: languageCode.optional().describe("languages içinde olmalı — proje dilleri hiç kurulmadıysa (ilk kurulumda) tek başına verilebilir. Runtime kayıtlı dil seçimi yokken buna düşer; boşsa aktif dil hiç seçilmez ve ilk açılışta bütün etiketler ham anahtar çizilir")
|
|
177
218
|
},
|
|
178
219
|
annotations: updates
|
|
179
220
|
}, handle(config, async (args) => {
|
|
180
221
|
const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'project', args, { operation: 'update' }));
|
|
222
|
+
// Çağıranın hatası okumadan ÖNCE döner: bozuk tema metni için projeyi çekmenin anlamı yok.
|
|
223
|
+
const requestedTheme = themeField(args.theme, 'project_update');
|
|
181
224
|
// Platform gövdede slug yoksa onu ADDAN yeniden üretiyor. Yalnız dil ayarını değiştiren bir
|
|
182
225
|
// çağrı bu yüzden 'test_dev' slug'ını 'mcp-benchmark' yapmıştı ve o an bütün runtime URL'leri
|
|
183
226
|
// kırılmıştı (09-05 Corecruiter koşusu, S-11). Gateway mevcut değerleri okuyup gövdeye koyar;
|
|
@@ -185,16 +228,15 @@ export function registerProjectTools(server, platform) {
|
|
|
185
228
|
const current = ((await platform.org('GET', `/projects/${args.id}`))?.data ?? {});
|
|
186
229
|
const currentSlug = typeof current.slug === 'string' ? current.slug : undefined;
|
|
187
230
|
const currentName = typeof current.name === 'string' ? current.name : undefined;
|
|
231
|
+
// Tema da aynı korumaya muhtaç ve aynı okumadan gelir — ek istek yok (S-1, projectUpdateForm).
|
|
232
|
+
const currentTheme = current.theme !== null && typeof current.theme === 'object' ? current.theme : undefined;
|
|
188
233
|
const name = args.name ?? currentName;
|
|
189
234
|
const slug = args.slug ?? currentSlug;
|
|
190
235
|
if (name === undefined) {
|
|
191
236
|
throw new PlatformApiError(`'${args.id}' projesinin adı okunamadı; name alanını açıkça verin (platform bu uçta adı zorunlu tutuyor).`, 400);
|
|
192
237
|
}
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
form.set('Slug', slug);
|
|
196
|
-
if (args.theme)
|
|
197
|
-
form.set('Theme', args.theme);
|
|
238
|
+
const theme = requestedTheme ?? (currentTheme === undefined ? undefined : JSON.stringify(currentTheme));
|
|
239
|
+
const form = projectUpdateForm({ name, slug, theme });
|
|
198
240
|
const languages = args.languages ? normalizeLanguageCodes(args.languages, 'languages') : undefined;
|
|
199
241
|
const defaultLanguage = args.defaultLanguage ? normalizeLanguageCode(args.defaultLanguage, 'defaultLanguage') : undefined;
|
|
200
242
|
languages?.codes.forEach(code => form.append('Languages', code));
|
|
@@ -212,6 +254,9 @@ export function registerProjectTools(server, platform) {
|
|
|
212
254
|
: []),
|
|
213
255
|
...(args.name === undefined && currentName !== undefined
|
|
214
256
|
? [`project_update gövdesine mevcut ad ('${currentName}') gateway tarafından kondu (name verilmedi).`]
|
|
257
|
+
: []),
|
|
258
|
+
...(args.theme === undefined && currentTheme !== undefined
|
|
259
|
+
? ["project_update gövdesine mevcut TEMA gateway tarafından kondu: platform Theme alanı gövdede yokken temayı SİLİYOR (Override koşulsuz yazar) ve silinen tema bütün ortamların runtime'ına iner — yalnız dil ayarını değiştiren bir çağrı markanın tamamını düşürüyordu (S-1). Temayı gerçekten temizlemek istiyorsanız theme: {} verin."]
|
|
215
260
|
: [])
|
|
216
261
|
];
|
|
217
262
|
return attachFindings(withNormalization(payload, notes), findings);
|
|
@@ -278,9 +323,20 @@ export function registerProjectTools(server, platform) {
|
|
|
278
323
|
// Dil durumu haritanın parçası: `pick` null/undefined alanları eler, yani dilsiz projede
|
|
279
324
|
// languages haritadan da sessizce düşerdi — eksikliği görünmez kılan tam o davranış (`S-15`).
|
|
280
325
|
const languageState = readProjectLanguages(projectPayload);
|
|
326
|
+
const themeSummary = readProjectTheme(projectPayload);
|
|
281
327
|
return {
|
|
282
328
|
project: pick(projectData, ['id', 'slug', 'name', 'languages', 'defaultLanguage']),
|
|
283
329
|
...(languageState ? { localization: localizationBlock(languageState) } : {}),
|
|
330
|
+
// Tema haritanın parçası: projenin markası olup olmadığı, sayfa kurmadan önce bilinmesi
|
|
331
|
+
// gereken bir şeydir ve `pick` onu göstermezdi.
|
|
332
|
+
theme: themeSummary === undefined
|
|
333
|
+
? { hasTheme: false }
|
|
334
|
+
: {
|
|
335
|
+
hasTheme: true,
|
|
336
|
+
mode: themeSummary.mode === 'dark' ? 'dark' : 'light',
|
|
337
|
+
presets: Object.keys(themeSummary.presets ?? {}),
|
|
338
|
+
componentPresets: Object.keys(themeSummary.componentPresets ?? {})
|
|
339
|
+
},
|
|
284
340
|
environments: (Array.isArray(projectData.environments) ? projectData.environments : []).map(entry => pick(entry, ['id', 'name'])),
|
|
285
341
|
environment: platform.resolveEnvironment(environment),
|
|
286
342
|
applications,
|
|
@@ -300,6 +356,7 @@ export function registerProjectTools(server, platform) {
|
|
|
300
356
|
...(languageState && !runtimeResolvesTranslations(languageState)
|
|
301
357
|
? [`DİKKAT: projenin dil ayarı eksik (localization bloğuna bakın) — çeviri yazsanız bile runtime her etiketi ham anahtar olarak basar. Etiketleri translation_* üzerinden verecekseniz ÖNCE ${localizationFix(languageState)}.`]
|
|
302
358
|
: []),
|
|
359
|
+
"Tema/preset'ler: theme_get (view:'resolved' ekrana inen --ng-* değerlerini verir).",
|
|
303
360
|
"Detay: entity_get/microservice_get/workflow_get (büyük kayıtlarda view:'outline'); değişiklik: *_patch. Automation'lar entity başına: automation_list(entityId) — ağaçtaki automationCount yalnız platform sayıyorsa görünür."
|
|
304
361
|
].join(' ')
|
|
305
362
|
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { isFlexContainer, schemaOpValidationErrors, FLEX_LAYOUT_PROP_NAMES, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
|
|
2
|
+
import { findNode, outlineAppliedPatch } from './screens.js';
|
|
3
|
+
import { setPropTypeErrors } from '../knowledge/propTypes.js';
|
|
4
|
+
import { PlatformApiError } from '../platform/errors.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
/*
|
|
7
|
+
* Şema micro-op'larının gateway tarafı: sayfa (`page_schema_patch`), layout (`layout_schema_patch`)
|
|
8
|
+
* ve widget (`component_schema_patch`) aynı sunucu sözlüğünü (`PageSchemaMicroOpApplier`) konuşur,
|
|
9
|
+
* bu yüzden op planlama ve istek öncesi reddetme tek yerde durur.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Yazma yanıtındaki şema gövdesinin biçimi (B-55 · S-9). Varsayılan `outline`: gövde ajanın
|
|
13
|
+
* aynı turda kendi gönderdiği içeriğin yankısıdır, kanıt ise pointer'da ve `silentlyDropped`ta
|
|
14
|
+
* durur. Bayrağı bilmeyen ajan pahalı olanı ödemesin diye kırpma OPT-OUT'tur (aynı karar:
|
|
15
|
+
* `DS-004`, `datasource_configuration_options`).
|
|
16
|
+
*/
|
|
17
|
+
export const writeViewParam = z.enum(['outline', 'full']).optional().describe("Yanıttaki şema gövdesinin biçimi. outline (VARSAYILAN): komponent ağaçları iskelete iner (id/elementId/name/type/children), appliedPatch her op'un op+path'ini verir — 'yalnız o komponente mi dokunuldu' kanıtı JSON Pointer'dadır, 'kaydedildi mi' kanıtı silentlyDropped'tadır. full: platformun döndürdüğü gövde aynen gelir.");
|
|
18
|
+
/**
|
|
19
|
+
* `appliedPatch` zarfın altında da gelebilir (`{ data: { appliedPatch } }`), düz de.
|
|
20
|
+
*/
|
|
21
|
+
export function outlineWriteResponse(response) {
|
|
22
|
+
const inner = response.data;
|
|
23
|
+
const container = (inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : response);
|
|
24
|
+
if (Array.isArray(container.appliedPatch))
|
|
25
|
+
container.appliedPatch = outlineAppliedPatch(container.appliedPatch);
|
|
26
|
+
}
|
|
27
|
+
export const opsParam = z.array(z.record(z.unknown())).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} (value ham değer, code sarmalı {type: 'code', ref: '{{ … }}'} ya da translation sarmalı {type: 'translation', ref: '<çeviri anahtarı>'} — prop tipini değiştirmek için komponenti silip yeniden EKLEMEYİN, ona yazan setter'ların hedefi silinir (FLOW-018); platform replace_prop'ta expression alanını OKUMAZ — gateway onu code sarmalına çevirir) | set_binding {target, prop, expression: '{{ Page.variables.x }}'} | add_component {parent (üst elementId ya da 'root'), index?, component} | remove_component {target} | move_component {target, parent, index?, position: {x, y}} (index ÇİZİM sırası değildir, runtime çocukları blocks.position'a göre (y, sonra x) çizer. page_schema_patch'te flex kapta gateway index'i görünen sıraya çevirir ve kardeş konumlarını yeniden yazar; ızgara kapta sırayı position.y verir, sona eklemeyen index PAGE-033 uyarısıdır. layout_schema_patch ve component_schema_patch bu çeviriyi YAPMAZ: orada sırayı position.y ile verin) | set_event {target, event, eventValue} | remove_event {target, event} (olayı diziden çıkarır; target: 'page' sayfa düzeyi olaylar. set_event ile eventValue: null olay SİLMEZ, değersiz ölü kayıt bırakır. Olmayan olay 422 missing-event) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler) | set_appearance {target, appearance: {heightType?, widthType?, tooltip?, background?, …}} (kurulu düğümün appearance'ına YALNIZ verilen alanları yazar, gerisini korur; null alanı temizler; bilinmeyen alan unknown-appearance-field ile reddedilir. GÖRÜNÜRLÜK appearance'ta DEĞİLDİR: gizlemek için replace_prop {target, prop: 'isHidden', value: true}, koşul için set_binding {prop: 'isHidden'} — add_component gövdesindeki appearance.isHidden'ı gateway props.isHidden'a taşır) | resize_component {target, size?: {w,h}, appearance?: {widthType,widthString,heightType,heightString}, states?: {isWrapper,isScrollable,isSticky,isDroppable,isFloating,isQuickStack}, breakpoint?: 1024} (gateway adı: platforma set_block {target, breakpoint, size} + set_appearance {target, appearance} + set_states {target, states} olarak gider — o üç op'u doğrudan da gönderebilirsiniz. move_component'e konan size reddedilir. Izgarada blocks[].size, flex'te appearance geçerlidir; ebeveynin düzeni yazılanı etkisiz kılıyorsa PAGE-019 uyarısı çıkar. states bir PROP DEĞİL düğüm alanıdır ve replace_prop onu yazamaz — düğümü silmeden isWrapper bayrağını değiştirmenin yolu budur). set_prop_type {target, prop, propType: 'static'|'code'|'translation', ref?, data?} (replace_prop sarmalıyla aynı sonucu verir ve mevcut data'yı tasarım yedeği olarak taşır. Bu op prop'un TİPİNİ yerinde değiştirir. Taşıyıcı alan tipe göre: static → data, code ve translation → ref (runtime data'yı her turda ezer, anahtar yalnız ref'te yaşar). Eksik taşıyıcı istek gönderilmeden reddedilir; page_schema_validate aynı kararı verir) | reorder {parent ('root' ya da elementId), order: [çocuk elementId'lerinin TAMAMI, yeni sırada]} (yalnız DİZİ sırasını değiştirir; çizim sırası blocks.position'dır — görünen sırayı değiştirmek için konumları set_block/move_component ile de yazın) | set_state_binding {target, binding: {name, expression, targets}} (ada göre ekler/değiştirir) | remove_state_binding {target, name} | batch {gestureId?, label?, ops: [...]} (iç içe batch yok). target = elementId. İstek atomiktir: tek op reddedilirse hiçbiri yazılmaz.");
|
|
28
|
+
/**
|
|
29
|
+
* PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
|
|
30
|
+
* komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
|
|
31
|
+
* tipini ve o tipin katalogdaki olay listesini pozitif olarak çözebildiğinde konuşur (okunan ağaçta
|
|
32
|
+
* olmayan hedefler — sayfa yamasında layout düğümleri — ve katalogda olay tanımlamayan tipler
|
|
33
|
+
* doğrulanmadan geçer).
|
|
34
|
+
*/
|
|
35
|
+
export function unknownEventOps(ops, typesByElementId, eventsBySlug) {
|
|
36
|
+
return ops
|
|
37
|
+
.map(op => op)
|
|
38
|
+
.filter(op => op.op === 'set_event')
|
|
39
|
+
.flatMap(op => {
|
|
40
|
+
const target = String(op.target ?? '');
|
|
41
|
+
const event = String(op.event ?? '');
|
|
42
|
+
const type = typesByElementId.get(target);
|
|
43
|
+
const declared = type ? eventsBySlug.get(type) : undefined;
|
|
44
|
+
if (!event || !declared || declared.includes(event))
|
|
45
|
+
return [];
|
|
46
|
+
return [`'${target}' (${type}) komponenti '${event}' diye bir olay YAYMAZ. Yaydıkları: ${declared.join(', ')}. Platform bu op'u 200 ile kabul eder ama bağ sessizce ölü kalır — akış hiçbir zaman çalışmaz. Doğru adı component_get(slug:'${type}') çıktısındaki events listesinden alın.`];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Ön kontroller eskiden İLK hatada fırlatıyordu: dört op'u yanlış olan bir yama dört ayrı turda
|
|
51
|
+
* ancak görülüyordu ve her tur tüm planı yeniden gönderiyordu (S-11, 2026-09-15). Artık op başına
|
|
52
|
+
* toplanır ve tek yanıtta döner. Yama yine ATOMİKTİR — birbirine bağlı op'lar (add_component +
|
|
53
|
+
* set_event) yarım kalmasın diye geçenler de gönderilmez.
|
|
54
|
+
*/
|
|
55
|
+
export function rejectedOpsMessage(unordered) {
|
|
56
|
+
const rejected = [...unordered].sort((left, right) => left.index - right.index);
|
|
57
|
+
const lines = rejected.map(entry => `#${entry.index} ${entry.op}${entry.target ? ` '${entry.target}'` : ''} [${entry.code}]: ${entry.message}`);
|
|
58
|
+
const validationHint = rejected.some(entry => entry.code === 'PAGE-016')
|
|
59
|
+
? " PAGE-016 çalışan biçimi dört alanı da taşır: { type: 'required'|'email'|'regex'|'max-length'|'min-length'|'max-value'|'min-value'|'value-options', message: '<kullanıcıya görünen metin>', value: <eşik/desen/true>, isActive: true }."
|
|
60
|
+
: '';
|
|
61
|
+
return `Ön kontrol ${rejected.length} op'u reddetti; yama atomik olduğu için HİÇBİR op gönderilmedi (istek gönderilmedi). Düşen op'ları düzeltip TÜM listeyi tek çağrıda yeniden gönderin.${validationHint}\n${lines.join('\n')}\nrejectedOps: ${JSON.stringify(rejected)}`;
|
|
62
|
+
}
|
|
63
|
+
export function collectTypes(nodes, into) {
|
|
64
|
+
for (const node of nodes ?? []) {
|
|
65
|
+
if (typeof node.elementId === 'string' && typeof node.type === 'string')
|
|
66
|
+
into.set(node.elementId, node.type);
|
|
67
|
+
collectTypes(node.children, into);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function removeNodeFromTree(nodes, target, parent) {
|
|
71
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
72
|
+
if (nodes[index].elementId === target)
|
|
73
|
+
return { node: nodes.splice(index, 1)[0], parent };
|
|
74
|
+
const found = removeNodeFromTree(nodes[index].children ?? [], target, nodes[index]);
|
|
75
|
+
if (found)
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
function setDesktopPosition(node, position) {
|
|
81
|
+
const blocks = Array.isArray(node.blocks) ? node.blocks : [];
|
|
82
|
+
const block = blocks.find(entry => Number(entry?.breakpoint) === 1024);
|
|
83
|
+
if (block)
|
|
84
|
+
block.position = { x: Number(position.x ?? 0), y: Number(position.y ?? 0) };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* PAGE-010 boşluk analizi patch'in SONUCUNU görmek zorunda: tek başına op'lardaki y değerleri,
|
|
88
|
+
* kapsayıcının mevcut çocukları bilinmeden anlamsız. Op'lar okunan şemanın kopyasına uygulanır
|
|
89
|
+
* (add: parent'ın children'ına ekle — kökler retargetRootAdds ile page_container'a gittiği için
|
|
90
|
+
* burada da öyle çözülür; move: düğümü taşı — parent verilmemişse aynı ebeveynde kalır — ve verilen
|
|
91
|
+
* position'ı desktop block'una yaz; ikisinde de index platformdaki gibi diziye yerleşir) ve analiz
|
|
92
|
+
* yalnız dokunulan kapsayıcılarda koşar.
|
|
93
|
+
*/
|
|
94
|
+
export function simulateLayoutOps(schema, ops) {
|
|
95
|
+
const touched = new Set();
|
|
96
|
+
const attach = (node, requestedParent, index) => {
|
|
97
|
+
const parentId = typeof requestedParent === 'string' && requestedParent !== '' && requestedParent !== 'root'
|
|
98
|
+
? requestedParent
|
|
99
|
+
: PAGE_CONTAINER_ELEMENT_ID;
|
|
100
|
+
const parent = findNode(schema, parentId);
|
|
101
|
+
const siblings = parent ? [...(parent.children ?? [])] : schema;
|
|
102
|
+
const insertAt = typeof index === 'number' && Number.isFinite(index) ? Math.max(0, Math.min(index, siblings.length)) : siblings.length;
|
|
103
|
+
siblings.splice(insertAt, 0, node);
|
|
104
|
+
if (parent) {
|
|
105
|
+
parent.children = siblings;
|
|
106
|
+
touched.add(String(parent.elementId ?? parentId));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
touched.add('kök');
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
for (const op of ops) {
|
|
113
|
+
if (op.op === 'add_component' && op.component && typeof op.component === 'object') {
|
|
114
|
+
attach(JSON.parse(JSON.stringify(op.component)), op.parent, op.index);
|
|
115
|
+
}
|
|
116
|
+
// Düzen prop'ları (layout/flex grubu) simülasyona girer: PAGE-010 flex kapsayıcının içini
|
|
117
|
+
// analiz etmez, PAGE-015 ise op'tan SONRAKİ layout değerine bakar — ikisi de op uygulanmadan
|
|
118
|
+
// doğru karar veremez.
|
|
119
|
+
if (op.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(op.prop ?? ''))) {
|
|
120
|
+
const target = findNode(schema, String(op.target ?? ''));
|
|
121
|
+
const codeWrapper = op.value && typeof op.value === 'object' && op.value.type === 'code';
|
|
122
|
+
if (target)
|
|
123
|
+
target.props = { ...(target.props ?? {}), [String(op.prop)]: codeWrapper ? op.value : { type: 'static', ref: null, data: op.value } };
|
|
124
|
+
}
|
|
125
|
+
if (op.op === 'move_component') {
|
|
126
|
+
const moved = removeNodeFromTree(schema, String(op.target ?? ''));
|
|
127
|
+
if (!moved)
|
|
128
|
+
continue;
|
|
129
|
+
if (op.position && typeof op.position === 'object')
|
|
130
|
+
setDesktopPosition(moved.node, op.position);
|
|
131
|
+
attach(moved.node, op.parent ?? moved.parent?.elementId ?? 'root', op.index);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return touched;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* F-8 — olay kaldırma 2026-09-14'e kadar gateway emülasyonuydu (şemayı okuyup tam PUT). Artık
|
|
138
|
+
* platformun `remove_event` op'u; `target: 'page'` sayfa düzeyi olayları hedefler ve olmayan olay
|
|
139
|
+
* sessizce yutulmaz, `missing-event` ile 422 döner.
|
|
140
|
+
*/
|
|
141
|
+
export function planEventRemovals(ops) {
|
|
142
|
+
return ops.map(op => {
|
|
143
|
+
const target = String(op.target ?? '');
|
|
144
|
+
const event = String(op.event ?? '');
|
|
145
|
+
if (!target || !event)
|
|
146
|
+
throw new PlatformApiError("remove_event için 'target' ve 'event' zorunludur.", 400);
|
|
147
|
+
return `${target}.${event}`;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function findParentNode(nodes, target) {
|
|
151
|
+
for (const node of nodes) {
|
|
152
|
+
if ((node.children ?? []).some(child => child.elementId === target))
|
|
153
|
+
return node;
|
|
154
|
+
const deeper = findParentNode(node.children ?? [], target);
|
|
155
|
+
if (deeper)
|
|
156
|
+
return deeper;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* G-25 — `states` bir prop değil düğüm alanı ve `replace_prop` onu yazamaz. Anahtar kümesi beyaz
|
|
162
|
+
* listeyle sınırlı: platform bilinmeyen anahtarı `unknown-states-field` ile reddeder ama ajan mesajı
|
|
163
|
+
* burada, istek gönderilmeden daha açık.
|
|
164
|
+
*/
|
|
165
|
+
const WRITABLE_STATES = new Set(['isWrapper', 'isScrollable', 'isSticky', 'isDroppable', 'isFloating', 'isQuickStack']);
|
|
166
|
+
/**
|
|
167
|
+
* `resize_component` gateway'in op adıdır (G-14): ızgarada boyut `blocks[].size`, flex'te
|
|
168
|
+
* `appearance`, bayraklar `states`. 2026-09-14'ten beri sunucu üçünü de kendi op'larıyla yazar
|
|
169
|
+
* (`set_block`, `set_appearance`, `set_states`); gateway yalnız çevirir. Tam GET + tam PUT yolu
|
|
170
|
+
* kalktı — bir sayfaya dokunan tek istek PATCH'tir.
|
|
171
|
+
*/
|
|
172
|
+
export function resizeServerOps(op) {
|
|
173
|
+
const target = String(op.target ?? '');
|
|
174
|
+
const size = op.size && typeof op.size === 'object' ? op.size : undefined;
|
|
175
|
+
const appearance = op.appearance && typeof op.appearance === 'object' ? op.appearance : undefined;
|
|
176
|
+
const states = op.states && typeof op.states === 'object' ? op.states : undefined;
|
|
177
|
+
return [
|
|
178
|
+
...(size ? [{ op: 'set_block', target, breakpoint: Number(op.breakpoint ?? 1024), size }] : []),
|
|
179
|
+
...(appearance ? [{ op: 'set_appearance', target, appearance }] : []),
|
|
180
|
+
...(states ? [{ op: 'set_states', target, states }] : [])
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Sunucu hedefi ve bloğu kendisi doğrular; burada okunan sayfadan yapılan kontrol ajana bağlamı
|
|
185
|
+
* olan bir mesaj ve PAGE-019/020 uyarıları içindir. `size` yalnız verilen kenarı taşır: eksik
|
|
186
|
+
* kenar okunan bloktan tamamlanır, yoksa sunucu 0 yazardı.
|
|
187
|
+
*/
|
|
188
|
+
export function planResizes(schema, ops, readTool = 'page_schema_get') {
|
|
189
|
+
const serverOps = [];
|
|
190
|
+
const applied = [];
|
|
191
|
+
const warnings = [];
|
|
192
|
+
for (const op of ops) {
|
|
193
|
+
const target = String(op.target ?? '');
|
|
194
|
+
const size = op.size && typeof op.size === 'object' ? op.size : undefined;
|
|
195
|
+
const appearance = op.appearance && typeof op.appearance === 'object' ? op.appearance : undefined;
|
|
196
|
+
const states = op.states && typeof op.states === 'object' ? op.states : undefined;
|
|
197
|
+
const breakpoint = Number(op.breakpoint ?? 1024);
|
|
198
|
+
if (!target)
|
|
199
|
+
throw new PlatformApiError("resize_component için 'target' zorunludur.", 400);
|
|
200
|
+
if (!size && !appearance && !states) {
|
|
201
|
+
throw new PlatformApiError(`resize_component '${target}' için 'size' ({ w, h } — ızgara), 'appearance' ({ widthType, widthString, heightType, heightString } — flex) ya da 'states' ({ isWrapper, isScrollable, isSticky, isDroppable, isFloating, isQuickStack }) verilmelidir; üçü de boş.`, 400);
|
|
202
|
+
}
|
|
203
|
+
if (states) {
|
|
204
|
+
const unknownStates = Object.keys(states).filter(key => !WRITABLE_STATES.has(key));
|
|
205
|
+
if (unknownStates.length > 0) {
|
|
206
|
+
throw new PlatformApiError(`resize_component '${target}': bilinmeyen states anahtarı (${unknownStates.join(', ')}). Yazılabilir bayraklar: ${[...WRITABLE_STATES].join(', ')}.`, 400);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const node = findNode(schema, target);
|
|
210
|
+
if (!node)
|
|
211
|
+
throw new PlatformApiError(`resize_component hedefi bulunamadı: '${target}'. ${readTool} ile elementId'leri listeleyin.`, 400);
|
|
212
|
+
const parent = findParentNode(schema, target);
|
|
213
|
+
const inFlex = parent !== undefined && isFlexContainer(parent);
|
|
214
|
+
let resolved = op;
|
|
215
|
+
if (size) {
|
|
216
|
+
const blocks = Array.isArray(node.blocks) ? node.blocks : [];
|
|
217
|
+
const block = blocks.find(entry => Number(entry?.breakpoint) === breakpoint);
|
|
218
|
+
if (!block) {
|
|
219
|
+
throw new PlatformApiError(`resize_component '${target}' düğümünde ${breakpoint} breakpoint'li blok yok (var olanlar: ${blocks.map(entry => entry?.breakpoint).join(', ') || 'hiç'}). Farklı bir breakpoint verin ya da düğümü add_component ile blokları kurulu olarak ekleyin.`, 400);
|
|
220
|
+
}
|
|
221
|
+
const current = (block.size ?? {});
|
|
222
|
+
const merged = { w: Number(size.w ?? current.w ?? 0), h: Number(size.h ?? current.h ?? 0) };
|
|
223
|
+
resolved = { ...op, size: merged };
|
|
224
|
+
applied.push(`${target}.size=${JSON.stringify(merged)}`);
|
|
225
|
+
if (inFlex) {
|
|
226
|
+
warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex kapsayıcı; orada yerleşimi 'blocks[].size' DEĞİL appearance (widthType/widthString/heightType/heightString) belirler. Boyut yazıldı ama ekranda etkisi olmayabilir — flex'te ölçü vermek için aynı op'a 'appearance' koyun.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (states) {
|
|
230
|
+
applied.push(`${target}.states=${JSON.stringify(states)}`);
|
|
231
|
+
if (states.isWrapper === true) {
|
|
232
|
+
warnings.push(`PAGE-020 (uyarı): '${target}' düğümüne isWrapper:true yazıldı — runtime sarmalayıcı düğüme 'min-height: 80vh' veriyor ve bu, sayfanın altına yüzlerce piksel ölü boşluk ekleyebilir (ölçüldü 09-05: dokuz düğüm ~1600px). style ile ezilemez; gerekmiyorsa isWrapper:false bırakın.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (appearance) {
|
|
236
|
+
applied.push(`${target}.appearance=${JSON.stringify(appearance)}`);
|
|
237
|
+
if (!inFlex && parent !== undefined) {
|
|
238
|
+
warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex DEĞİL; ızgarada yerleşimi 'blocks[].size' belirler ve appearance genişlik/yükseklik alanları etkisiz kalabilir. Izgarada ölçü vermek için aynı op'a 'size' koyun.`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
serverOps.push(...resizeServerOps(resolved));
|
|
242
|
+
}
|
|
243
|
+
return { serverOps, applied, warnings };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* `set_prop_type` (P0-2) 2026-09-14'ten beri platformun op'u; taşıyıcı kuralı
|
|
247
|
+
* (`PROP_TYPE_CARRIER`) iki tarafta aynı. Hedef okunan sayfada aranır ki ajan "bulunamadı"yı
|
|
248
|
+
* bağlamıyla alsın; `value` eşanlamlısı sunucunun `data` alanına taşınır.
|
|
249
|
+
*/
|
|
250
|
+
export function planPropTypes(schema, ops, readTool = 'page_schema_get') {
|
|
251
|
+
const missing = ops.map(op => String(op.target ?? '')).find(target => !findNode(schema, target));
|
|
252
|
+
if (missing !== undefined) {
|
|
253
|
+
throw new PlatformApiError(`set_prop_type hedefi bulunamadı: '${missing}'. ${readTool} ile elementId'leri listeleyin.`, 400);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
serverOps: ops.map(({ value, ...op }) => (op.data === undefined && value !== undefined ? { ...op, data: value } : op)),
|
|
257
|
+
applied: ops.map(op => {
|
|
258
|
+
const current = findNode(schema, String(op.target))?.props;
|
|
259
|
+
return `${op.target}.${op.prop}: ${current?.[String(op.prop)]?.type ?? '(yok)'} → ${op.propType}`;
|
|
260
|
+
})
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* İstek gitmeden reddedilen gövde hataları. `normalizeSchemaOps` ÖNCE koşmalıdır: PAGE-016 yalnız
|
|
265
|
+
* normalizasyonun tamamlayamadığı validation gövdelerini sayar.
|
|
266
|
+
*/
|
|
267
|
+
export function assertOpBodies(opRecords, propTypeOps) {
|
|
268
|
+
const propTypeIssues = setPropTypeErrors(propTypeOps);
|
|
269
|
+
if (propTypeIssues.length > 0)
|
|
270
|
+
throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
|
|
271
|
+
const sizedMoves = opRecords.filter(entry => entry.op === 'move_component' && entry.size !== undefined);
|
|
272
|
+
if (sizedMoves.length > 0) {
|
|
273
|
+
throw new PlatformApiError(`move_component yalnız 'position' uygular; gövdeye konan 'size' platformda SESSİZCE düşer (ölçüldü 2026-09-04, appliedPatch'te yalnız /blocks/0/position görünür). Boyut için resize_component kullanın: { op: 'resize_component', target: '<elementId>', size: { w, h } } — flex kapsayıcı içindeyse 'appearance'. Etkilenen hedefler: ${sizedMoves.map(entry => entry.target).join(' · ')}`, 400);
|
|
274
|
+
}
|
|
275
|
+
const validationIssues = schemaOpValidationErrors(opRecords);
|
|
276
|
+
if (validationIssues.length > 0) {
|
|
277
|
+
throw new PlatformApiError(`PAGE-016 — validation gövdesi eksik (istek gönderilmedi): ${validationIssues.join(' · ')}. `
|
|
278
|
+
+ "Çalışan biçim dört alanı da taşır: { type: 'required'|'email'|'regex'|'max-length'|'min-length'|'max-value'|'min-value'|'value-options', message: '<kullanıcıya görünen metin>', value: <eşik/desen/true>, isActive: true }.", 400);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -4,7 +4,8 @@ import { applyCatalogLayoutRules } from './components.js';
|
|
|
4
4
|
import { PlatformApiError } from '../platform/errors.js';
|
|
5
5
|
const GUARD_CODES = {
|
|
6
6
|
page: { events: 'PAGE-011', widgets: 'PAGE-012', noun: 'sayfa', tree: 'schema' },
|
|
7
|
-
layout: { events: 'LAY-004', widgets: 'LAY-005', noun: 'layout', tree: 'views' }
|
|
7
|
+
layout: { events: 'LAY-004', widgets: 'LAY-005', noun: 'layout', tree: 'views' },
|
|
8
|
+
widget: { events: 'PAGE-011', widgets: 'PAGE-012', noun: 'widget', tree: 'schema' }
|
|
8
9
|
};
|
|
9
10
|
/**
|
|
10
11
|
* JS tanımlayıcısı olmayan ya da AYRILMIŞ `name` alanlarını toplar; ad
|
package/dist/tools/screens.js
CHANGED
|
@@ -152,10 +152,3 @@ export async function fetchHost(platform, host, project, environment, applicatio
|
|
|
152
152
|
}
|
|
153
153
|
return record;
|
|
154
154
|
}
|
|
155
|
-
export function hostUpdateBody(host, record, overrides = {}) {
|
|
156
|
-
if (host === 'page')
|
|
157
|
-
return pageUpdateBody(record, overrides);
|
|
158
|
-
if (host === 'layout')
|
|
159
|
-
return layoutUpdateBody(record, overrides);
|
|
160
|
-
return componentUpdateBody(record, overrides);
|
|
161
|
-
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { guid, projectParam, readOnly, updates } from './common.js';
|
|
2
|
+
import { diffCssVars, mergeRadiusScale, mergeSpacingScale, mergeTypeScale, resolveTheme } from '../knowledge/themeResolve.js';
|
|
3
|
+
import { diffThemePaths, mergeTheme, normalizeTheme, readProjectTheme, summarizeTheme, themeInput, unsetThemePaths } from '../knowledge/themeSchema.js';
|
|
4
|
+
import { componentPresetTypeWarnings, themeInputWarnings, unsetWarnings } from '../knowledge/themeRules.js';
|
|
5
|
+
import { attachFindings, enforcePolicy, evaluatePolicy, applyPolicyMode } from '../knowledge/policy.js';
|
|
6
|
+
import { projectUpdateForm } from './projects.js';
|
|
7
|
+
import { withNormalization } from '../knowledge/normalize.js';
|
|
8
|
+
import { PlatformApiError } from '../platform/errors.js';
|
|
9
|
+
import { policyRules } from '../knowledge/rules.js';
|
|
10
|
+
import { handle } from '../platform/responses.js';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
const modeParam = z.enum(['light', 'dark']);
|
|
13
|
+
/**
|
|
14
|
+
* Tema PROJE kaydıdır (`projects.theme` jsonb), ortam başına değil: okuma da yazma da
|
|
15
|
+
* Organization.API'ye gider ve `environment` parametresi YOKTUR. Kayıt her aktif ortamın tenant
|
|
16
|
+
* kopyasına platform tarafından yayılır (`UpdateProjectWhenOrganizationProjectUpdatedEventHandler`).
|
|
17
|
+
*/
|
|
18
|
+
async function loadProject(platform, project) {
|
|
19
|
+
const slug = platform.resolveProject(project);
|
|
20
|
+
const payload = await platform.org('GET', `/p/${slug}`);
|
|
21
|
+
const data = (payload?.data ?? {});
|
|
22
|
+
return {
|
|
23
|
+
payload,
|
|
24
|
+
project: { id: data.id, slug: data.slug, name: data.name },
|
|
25
|
+
theme: readProjectTheme(payload)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const EMPTY_THEME_NOTE = 'Proje TEMASIZ — runtime platformun kendi varsayılanını çizer (accent #0052CC, Inter 14px, '
|
|
29
|
+
+ 'radius 4px, spacing 10px). Marka için theme_update; yalnız TEK bir sayfada sapma istiyorsanız '
|
|
30
|
+
+ "page_container.style içinde --ng-* değişkenlerini ezin (platform_guide('page') Kural 1h).";
|
|
31
|
+
export function registerThemeTools(server, platform) {
|
|
32
|
+
const config = platform.config;
|
|
33
|
+
server.registerTool('theme_get', {
|
|
34
|
+
title: 'Proje temasını getir',
|
|
35
|
+
description: "Projenin tema kaydını döner: hangi alanlara dokunulmuş (summary), kaydın kendisi (raw) ya da EKRANA İNEN hâli (resolved — tam palet + birleşik ölçekler + runtime'ın bastığı bütün --ng-* değişkenleri). 'Accent'i ezmedim, ekranda hangi mavi çiziliyor' sorusunun cevabı resolved kipidir. Tema PROJE kaydıdır: ortam parametresi yoktur, bütün ortamların runtime'ı aynı temayı çizer.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
project: projectParam,
|
|
38
|
+
view: z.enum(['summary', 'raw', 'resolved']).optional().describe("summary (varsayılan): dokunulmuş alanların dökümü + preset/bileşen preset listesi. raw: tema kaydı olduğu gibi. resolved: palet, token'lar, gölge kademeleri ve cssVars (--ng-* → değer)"),
|
|
39
|
+
mode: modeParam.optional().describe("Yalnız resolved: hangi mod çözülsün. Verilmezse kaydın kendi mode'u (yoksa light). Palet override'ları mod başına tutulur (palette / paletteDark), bu yüzden iki mod ayrı ayrı çözülür")
|
|
40
|
+
},
|
|
41
|
+
annotations: readOnly
|
|
42
|
+
}, handle(config, async ({ project, view, mode }) => {
|
|
43
|
+
const { project: projectInfo, theme } = await loadProject(platform, project);
|
|
44
|
+
const storedMode = theme?.mode === 'dark' ? 'dark' : 'light';
|
|
45
|
+
const head = { project: projectInfo, hasTheme: theme !== undefined, storedMode };
|
|
46
|
+
if (view === 'raw') {
|
|
47
|
+
return {
|
|
48
|
+
...head,
|
|
49
|
+
theme: theme ?? {},
|
|
50
|
+
note: theme === undefined
|
|
51
|
+
? EMPTY_THEME_NOTE
|
|
52
|
+
: "Kayıt olduğu gibi. Ekrana inen değerler için view:'resolved'; kısmi yazma için theme_update."
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (view === 'resolved') {
|
|
56
|
+
const resolved = resolveTheme(theme, mode);
|
|
57
|
+
return {
|
|
58
|
+
...head,
|
|
59
|
+
mode: resolved.mode,
|
|
60
|
+
palette: resolved.palette,
|
|
61
|
+
// Alan adı 'tokens' OLAMAZ: `maskSecrets` anahtarında 'token' geçen her değeri '***' ile
|
|
62
|
+
// değiştiriyor (`secretKeyPattern`, platform/secrets.ts) ve yanıt sessizce boşalıyordu —
|
|
63
|
+
// ölçüldü 2026-09-15, tema token'ları tasarım değeri, gizli bilgi değil.
|
|
64
|
+
values: {
|
|
65
|
+
fontFamily: resolved.tokens.fontFamily,
|
|
66
|
+
fontHeading: resolved.tokens.headingFontFamily ?? resolved.tokens.fontFamily,
|
|
67
|
+
fontSize: resolved.tokens.fontSize,
|
|
68
|
+
fontWeight: resolved.tokens.fontWeight,
|
|
69
|
+
spacing: resolved.tokens.spacing,
|
|
70
|
+
borderRadius: resolved.tokens.borderRadius,
|
|
71
|
+
defaultInputSize: resolved.tokens.defaultInputSize,
|
|
72
|
+
typeScale: mergeTypeScale(resolved.tokens.typeScale),
|
|
73
|
+
radiusScale: mergeRadiusScale(resolved.tokens.radiusScale),
|
|
74
|
+
spacingScale: mergeSpacingScale(resolved.tokens.spacingScale),
|
|
75
|
+
shadow: resolved.tokens.shadow
|
|
76
|
+
},
|
|
77
|
+
shadow: resolved.shadow,
|
|
78
|
+
cssVars: resolved.cssVars,
|
|
79
|
+
presets: Object.keys(theme?.presets ?? {}),
|
|
80
|
+
componentPresets: Object.keys(theme?.componentPresets ?? {}),
|
|
81
|
+
note: "cssVars, Theme.svelte'nin her sayfaya bastığı değişkenlerdir; bir sayfanın page_container.style'ı aynı adı yeniden tanımlarsa O SAYFADA o kazanır. Koyu mod için mode:'dark' — ziyaretçinin kendi tercihi (theme-toggle, localStorage) runtime'da projenin modunun önüne geçer."
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (theme === undefined)
|
|
85
|
+
return { ...head, note: EMPTY_THEME_NOTE };
|
|
86
|
+
const summary = summarizeTheme(theme);
|
|
87
|
+
return {
|
|
88
|
+
...head,
|
|
89
|
+
...summary,
|
|
90
|
+
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
|
+
};
|
|
92
|
+
}));
|
|
93
|
+
server.registerTool('theme_update', {
|
|
94
|
+
title: 'Proje temasını güncelle',
|
|
95
|
+
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.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
project: projectParam,
|
|
98
|
+
theme: themeInput.optional().describe("Yazılacak alanlar — KISMİ. Nesneler derin birleşir, dizi/ilkel değer yerine geçer. Tanınmayan anahtar (ör. 'pallete') istek gitmeden reddedilir: platform temayı doğrulamayan bir jsonb blob'u olarak saklar ve yazım hatası kayıtta sessizce yaşardı"),
|
|
99
|
+
unset: z.array(z.string().min(1)).optional().describe("Silinecek nokta yolları: 'presets.kart', 'componentPresets.button.versions.V2', 'headingFontFamily', 'paletteDark.accent.primary'. Silmenin kanalı budur — null yazmak silmez"),
|
|
100
|
+
replace: z.boolean().optional().describe("true: 'theme' mevcut kaydın YERİNE geçer (birleştirme yok). Temayı sıfırlamak: replace: true, theme: {}"),
|
|
101
|
+
applicationId: guid.optional().describe('Verilirse componentPresets tipleri o uygulamanın kataloğuna göre denetlenir (THEME-011); verilmezse bu kontrol atlanır'),
|
|
102
|
+
dryRun: z.boolean().optional().describe('Yazmadan aynı yanıtı döner: changed + cssVarsChanged + lint bulguları. Yazmadan önce ekranda ne değişeceğini görmenin yolu budur'),
|
|
103
|
+
policyOverride: z.boolean().optional().describe('block bulgularını warn\'a indirir (bilinçli istisna)')
|
|
104
|
+
},
|
|
105
|
+
annotations: updates
|
|
106
|
+
}, handle(config, async ({ project, theme, unset, replace, applicationId, dryRun, policyOverride }) => {
|
|
107
|
+
if (theme === undefined && (unset === undefined || unset.length === 0)) {
|
|
108
|
+
throw new PlatformApiError("theme_update: 'theme' ya da 'unset' parametrelerinden en az biri dolu olmalı. Temayı sıfırlamak için replace: true, theme: {} verin.", 400);
|
|
109
|
+
}
|
|
110
|
+
const { project: projectInfo, payload, theme: base } = await loadProject(platform, project);
|
|
111
|
+
const data = (payload?.data ?? {});
|
|
112
|
+
const name = typeof data.name === 'string' ? data.name : undefined;
|
|
113
|
+
const slug = typeof data.slug === 'string' ? data.slug : undefined;
|
|
114
|
+
if (name === undefined || data.id === undefined) {
|
|
115
|
+
throw new PlatformApiError(`theme_update: '${platform.resolveProject(project)}' projesi okunamadı (ad ya da id yok); project_get ile kontrol edin.`, 400);
|
|
116
|
+
}
|
|
117
|
+
const patch = (theme ?? {});
|
|
118
|
+
const current = base ?? {};
|
|
119
|
+
const merged = replace === true ? patch : mergeTheme(current, patch);
|
|
120
|
+
const removal = unsetThemePaths(merged, unset ?? []);
|
|
121
|
+
const normalized = normalizeTheme(removal.theme);
|
|
122
|
+
const next = normalized.theme;
|
|
123
|
+
const findings = [...enforcePolicy(evaluatePolicy(config, policyRules, 'theme', next, { operation: 'update' }, policyOverride))];
|
|
124
|
+
// Uyarılar `policyRules` dışında üretiliyor (girdiye ve platform okumasına bakıyorlar), bu
|
|
125
|
+
// yüzden POLICY kipini ve policyOverride'ı kendileri uygulamak zorunda — WF-031 ile aynı yol.
|
|
126
|
+
if (config.policyMode !== 'off') {
|
|
127
|
+
const warnings = [
|
|
128
|
+
...themeInputWarnings(patch, next),
|
|
129
|
+
...unsetWarnings(current, unset ?? [])
|
|
130
|
+
];
|
|
131
|
+
if (applicationId !== undefined && Object.keys(next.componentPresets ?? {}).length > 0) {
|
|
132
|
+
const catalog = await platform.tenant('GET', project, `/components/${applicationId}`, { query: { pageIndex: 0, pageLength: 999 } })
|
|
133
|
+
.catch(() => undefined);
|
|
134
|
+
if (catalog?.data)
|
|
135
|
+
warnings.push(...componentPresetTypeWarnings(next, catalog.data));
|
|
136
|
+
}
|
|
137
|
+
findings.push(...applyPolicyMode(config, warnings, policyOverride));
|
|
138
|
+
}
|
|
139
|
+
const before = resolveTheme(current).cssVars;
|
|
140
|
+
const after = resolveTheme(next).cssVars;
|
|
141
|
+
const changed = diffThemePaths(current, next);
|
|
142
|
+
const head = {
|
|
143
|
+
project: projectInfo,
|
|
144
|
+
changed: { ...changed, ...(removal.missing.length > 0 ? { missing: removal.missing } : {}) },
|
|
145
|
+
cssVarsChanged: diffCssVars(before, after),
|
|
146
|
+
theme: summarizeTheme(next)
|
|
147
|
+
};
|
|
148
|
+
const panelWarning = "Studio tema paneli AÇIK ve kaydedilmemişse panelin sonraki Save'i bu yazmayı ezer; panel kapalıyken yazın.";
|
|
149
|
+
const note = `Tema PROJE kaydıdır: bu değişiklik bütün ortamların runtime'ına indi (production dâhil). Kanıt: page_screenshot — ikinci mod için colorScheme:'dark'. ${panelWarning}`;
|
|
150
|
+
if (dryRun === true) {
|
|
151
|
+
return attachFindings(withNormalization({
|
|
152
|
+
...head,
|
|
153
|
+
dryRun: true,
|
|
154
|
+
// Kuru koşunun notu GELECEK zaman kurar: yazma notunu "YAZILMADI" önekiyle sunmak
|
|
155
|
+
// aynı cümlede iki şey söylüyordu ("yazılmadı" + "indi") — ölçüldü 2026-09-15 canlı koşu.
|
|
156
|
+
note: `YAZILMADI (dryRun) — bu çağrı hiçbir şey değiştirmedi. Yazarsanız değişiklik bütün ortamların runtime'ına iner (production dâhil) ve ekranda page_screenshot ile doğrulanır (ikinci mod için colorScheme:'dark'). ${panelWarning}`
|
|
157
|
+
}, normalized.fixes), findings);
|
|
158
|
+
}
|
|
159
|
+
const form = projectUpdateForm({ name, slug, theme: JSON.stringify(next) });
|
|
160
|
+
const response = await platform.org('PUT', `/projects/${String(data.id)}`, { form });
|
|
161
|
+
return attachFindings(withNormalization({ ...head, updated: true, note, platform: response }, normalized.fixes), findings);
|
|
162
|
+
}));
|
|
163
|
+
}
|