@appaflytech/zeyrek-ai-mcp 0.0.7 → 0.0.8
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/dist/knowledge/backendConstraints.js +3 -0
- package/dist/knowledge/componentCatalog.js +59 -15
- package/dist/knowledge/normalize.js +459 -23
- package/dist/knowledge/propTypes.js +5 -4
- package/dist/knowledge/rules.js +110 -5
- package/dist/knowledge/styleResolve.js +6 -1
- package/dist/platform/http.js +10 -2
- package/dist/platform/screenshot.js +80 -8
- package/dist/platform/strictArguments.js +32 -0
- package/dist/platform/writeDiff.js +6 -3
- package/dist/resources/examples/page-sign-in.json +1 -1
- package/dist/resources/guides/component.md +1 -1
- package/dist/resources/guides/expressions.md +6 -0
- package/dist/resources/guides/getting-started.md +2 -0
- package/dist/resources/guides/layout.md +6 -0
- package/dist/resources/guides/microservice.md +105 -5
- package/dist/resources/guides/page.md +81 -18
- package/dist/resources/guides/translation.md +11 -10
- package/dist/resources/guides/workflow.md +50 -1
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/CreateMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/CreateWorkflowCommand.json +4 -1
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/PreviewWorkflowCommand.json +4 -1
- package/dist/resources/schemas/StepDto.json +4 -1
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/UpdateWorkflowCommand.json +4 -1
- package/dist/server.js +2 -1
- package/dist/tools/flows.js +9 -3
- package/dist/tools/layouts.js +4 -4
- package/dist/tools/microservices.js +70 -6
- package/dist/tools/pages.js +127 -57
- package/dist/tools/queries.js +72 -6
- package/dist/tools/workflows.js +9 -5
- package/package.json +1 -1
|
@@ -76,6 +76,8 @@ const executeInput = {
|
|
|
76
76
|
query: z.record(z.unknown()).optional(),
|
|
77
77
|
body: z.union([z.record(z.unknown()), z.array(z.record(z.unknown()))]).optional()
|
|
78
78
|
.describe('İstek gövdesi: tek nesne ya da nesne dizisi (bulk uçlar için)'),
|
|
79
|
+
parameters: z.record(z.unknown()).optional()
|
|
80
|
+
.describe("Ucun çağrı parametreleri (ad → değer) — query kaynaklı ucun sorgu parametreleri ve tablo ucunun filtreleri. Platform değeri body → query-string → header sırasıyla arar; gateway GET/DELETE'te query-string'e, POST/PUT'ta gövdeye koyar ve nereye koyduğunu gatewayNormalized ile söyler. Aynı ad 'query'/'body'de FARKLI değerle de verilirse çağrı gönderilmez. Boş string platformda NULL'a çevrilir."),
|
|
79
81
|
headers: z.record(z.string()).optional().describe("İsteğe eklenecek header'lar. 'Authorization' verirseniz gateway KENDİ token'ını KOYMAZ — istek tamamen sizin kimliğinizle gider (middleware'li uçları üye token'ıyla test etmenin yolu budur)"),
|
|
80
82
|
anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı olmadan gider — 'korumalı uç tokensız ne döner?' sorusunun cevabı budur (korumalı uçtan 401, anonim uçtan 200 beklenir). Gateway normalde her çağrıya kendi token'ını koyduğu için bu bayrak olmadan anonim erişim testi yapılamaz."),
|
|
81
83
|
asUser: asUserParam,
|
|
@@ -133,6 +135,56 @@ async function middlewareChain(platform, project, environment, application, micr
|
|
|
133
135
|
return { middlewareNote: `Middleware zinciri çözülemedi: ${error instanceof Error ? error.message : String(error)}. Uca bağlı middleware'leri microservice_get ile okuyabilirsiniz.` };
|
|
134
136
|
}
|
|
135
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* G-13 — üst düzey `parameters` isteğin platformun okuduğu yerine bağlanır.
|
|
140
|
+
*
|
|
141
|
+
* Platform çağrı parametresini `MicroserviceExecuteParameter.FindValue` ile body → query-string →
|
|
142
|
+
* header sırasıyla arar. GET/DELETE isteğinde gövde taşınmadığı için değer query-string'e, yazan
|
|
143
|
+
* yöntemde gövdeye (tek nesne) konur; gövdede tipi (sayı, bool) korunur. Aynı ad çağrıda iki
|
|
144
|
+
* farklı değerle geldiğinde hangisinin kazanacağını tahmin ettirmek yerine istek gönderilmez.
|
|
145
|
+
*/
|
|
146
|
+
function bindCallParameters(method, parameters, query, body, notes) {
|
|
147
|
+
const names = Object.keys(parameters ?? {});
|
|
148
|
+
if (parameters === undefined || names.length === 0)
|
|
149
|
+
return { query, body };
|
|
150
|
+
const intoQuery = method === 'GET' || method === 'DELETE';
|
|
151
|
+
if (!intoQuery && Array.isArray(body)) {
|
|
152
|
+
throw new PlatformApiError(`parameters (${names.join(', ')}) ${method} isteğinde gövdeye bağlanır, ama body bir DİZİ (bulk). Platform parametreyi yalnız gövdenin İLK öğesinden okur — değerleri body[0]'a yazın ya da parameters'ı kaldırın.`, 400);
|
|
153
|
+
}
|
|
154
|
+
const target = (intoQuery ? query : body) ?? {};
|
|
155
|
+
const conflicts = names.filter(name => name in target && JSON.stringify(target[name]) !== JSON.stringify(parameters[name]));
|
|
156
|
+
if (conflicts.length > 0) {
|
|
157
|
+
throw new PlatformApiError(`parameters ile ${intoQuery ? 'query' : 'body'} aynı adı farklı değerle taşıyor: ${conflicts.map(name => `'${name}'`).join(', ')}. Değeri tek yerde verin.`, 400);
|
|
158
|
+
}
|
|
159
|
+
const merged = { ...target, ...parameters };
|
|
160
|
+
notes.push(`parameters ${intoQuery ? "query-string'e" : 'gövdeye'} bağlandı (${method}): ${names.join(', ')}`);
|
|
161
|
+
return intoQuery ? { query: merged, body } : { query, body: merged };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Debug ağacında query düğümünün parametre satırı değer NULL bağlandığında `value` taşır ama
|
|
165
|
+
* gateway yanıtı `prune` ile null alanlardan temizlediği için satır ANAHTARSIZ görünür — G-13'te
|
|
166
|
+
* kökü veren tek iz bu yokluktu. Yokluğu okumak zorunda kalmamak için satırlar adıyla listelenir.
|
|
167
|
+
*/
|
|
168
|
+
function unboundQueryParameters(value, into = []) {
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
for (const entry of value)
|
|
171
|
+
unboundQueryParameters(entry, into);
|
|
172
|
+
return into;
|
|
173
|
+
}
|
|
174
|
+
if (!value || typeof value !== 'object')
|
|
175
|
+
return into;
|
|
176
|
+
const node = value;
|
|
177
|
+
if (node.type === 'query' && Array.isArray(node.input)) {
|
|
178
|
+
for (const row of node.input) {
|
|
179
|
+
if (row?.source === 'query' && row.member === 'parameter' && (row.value === null || row.value === undefined || row.value === '')) {
|
|
180
|
+
into.push({ step: String(node.name ?? '(adsız query)'), parameter: String(row.expression ?? '') });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const entry of Object.values(node))
|
|
185
|
+
unboundQueryParameters(entry, into);
|
|
186
|
+
return into;
|
|
187
|
+
}
|
|
136
188
|
function executePath(prefix, application, microservice, routeParams) {
|
|
137
189
|
const routeSuffix = routeParams && routeParams.length > 0
|
|
138
190
|
? `/${routeParams.map(encodeURIComponent).join('/')}`
|
|
@@ -325,21 +377,33 @@ export function registerMicroserviceTools(server, platform) {
|
|
|
325
377
|
description: "Kayıtlı microservice'i gerçek runtime yolundan çalıştırır (POST /{project}/m/{application}/{microservice SLUG'ı}). Dönen httpStatus microservice'in kendi sonucudur (hata da geçerli çıktıdır); beklenmedik sonuçta microservice_debug ile adım adım inceleyin. Anonim erişim testi için anonymous: true. Yanıt 'headers' alanında platformun döndüğü header'ları taşır (küçük harf; ör. headers['total-count'] — sayfalı tablo akışlarının okuduğu sözleşme; set-cookie/authorization elenir).",
|
|
326
378
|
inputSchema: executeInput,
|
|
327
379
|
annotations: writes
|
|
328
|
-
}, handle(config, async ({ project, environment, application, microservice, method, routeParams, query, body, headers, anonymous, asUser, cookies, language }) => {
|
|
380
|
+
}, handle(config, async ({ project, environment, application, microservice, method, routeParams, query, body, parameters, headers, anonymous, asUser, cookies, language }) => {
|
|
329
381
|
const notes = [];
|
|
330
382
|
const requestHeaders = { ...runIdentity(anonymous, asUser, cookies, headers), ...languageHeader(language, notes) };
|
|
331
|
-
|
|
383
|
+
const bound = bindCallParameters(method, parameters, query, body, notes);
|
|
384
|
+
return withNormalization(await platform.tenantRaw(method, project, executePath('m', application, microservice, routeParams), { environment, query: bound.query, body: bound.body, headers: requestHeaders, anonymous, mirrorAuthorizationCookie: true }), notes);
|
|
332
385
|
}));
|
|
333
386
|
server.registerTool('microservice_debug', {
|
|
334
387
|
title: 'Microservice debug',
|
|
335
|
-
description: "Microservice'i debug modunda çalıştırır; yanıtta adım adım executionItems ağacı döner (her adımın girdi/çıktısı). executionItems YALNIZ ucun kendi adımlarını kapsar — ucun ÖNÜNDE koşan middleware'ler ayrıca 'middlewares' alanında adım özetiyle listelenir (09-04 Y-04).",
|
|
388
|
+
description: "Microservice'i debug modunda çalıştırır; yanıtta adım adım executionItems ağacı döner (her adımın girdi/çıktısı). executionItems YALNIZ ucun kendi adımlarını kapsar — ucun ÖNÜNDE koşan middleware'ler ayrıca 'middlewares' alanında adım özetiyle listelenir (09-04 Y-04). Değeri NULL bağlanan query parametreleri 'unboundParameters' alanında adıyla listelenir (ağaçta bu satırların value anahtarı görünmez).",
|
|
336
389
|
inputSchema: executeInput,
|
|
337
390
|
annotations: writes
|
|
338
|
-
}, handle(config, async ({ project, environment, application, microservice, method, routeParams, query, body, headers, anonymous, asUser, cookies, language }) => {
|
|
391
|
+
}, handle(config, async ({ project, environment, application, microservice, method, routeParams, query, body, parameters, headers, anonymous, asUser, cookies, language }) => {
|
|
339
392
|
const notes = [];
|
|
340
393
|
const requestHeaders = { ...runIdentity(anonymous, asUser, cookies, headers), ...languageHeader(language, notes) };
|
|
341
|
-
const
|
|
342
|
-
|
|
394
|
+
const bound = bindCallParameters(method, parameters, query, body, notes);
|
|
395
|
+
const payload = await platform.tenantRaw(method, project, executePath('debug', application, microservice, routeParams), { environment, query: bound.query, body: bound.body, headers: requestHeaders, anonymous, mirrorAuthorizationCookie: true });
|
|
396
|
+
const unbound = unboundQueryParameters(payload.body);
|
|
397
|
+
return withNormalization({
|
|
398
|
+
...payload,
|
|
399
|
+
...(unbound.length > 0
|
|
400
|
+
? {
|
|
401
|
+
unboundParameters: unbound,
|
|
402
|
+
unboundParametersNote: "Bu query parametreleri NULL bağlandı: çağrıda yoklar ya da değerleri boş string'di (platform boş string'i NULL'a çevirir). Parametreyi 'parameters' (ya da GET'te 'query', POST/PUT'ta 'body') ile verin; ad sorgunun parametre adıyla birebir aynı olmalı."
|
|
403
|
+
}
|
|
404
|
+
: {}),
|
|
405
|
+
...await middlewareChain(platform, project, environment, application, microservice, payload.httpStatus)
|
|
406
|
+
}, notes);
|
|
343
407
|
}));
|
|
344
408
|
server.registerTool('microservice_preview', {
|
|
345
409
|
title: 'Microservice önizle',
|
package/dist/tools/pages.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive, microOps, isBranchName } from './common.js';
|
|
2
|
-
import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, staleSetterWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, isFlexContainer, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, withNormalization, parseVariableEntries, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
|
|
2
|
+
import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, staleSetterWarnings, lostSetterTargets, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, codePropTemplateErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, spacingLayerWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, isFlexContainer, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, flexIndexOrder, flexIndexOrderNote, withNormalization, parseVariableEntries, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
|
|
3
3
|
import { functionSignature } from '../knowledge/functionGuards.js';
|
|
4
4
|
import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, outlineAppliedPatch, findNode, flowPath } from './screens.js';
|
|
5
5
|
import { fetchComponentEventNames } from './components.js';
|
|
@@ -51,7 +51,7 @@ function outlineWriteResponse(response) {
|
|
|
51
51
|
if (Array.isArray(container.appliedPatch))
|
|
52
52
|
container.appliedPatch = outlineAppliedPatch(container.appliedPatch);
|
|
53
53
|
}
|
|
54
|
-
const opsParam = z.array(z.record(z.unknown())).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} | 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}} | set_event {target, event, eventValue} | remove_event {target, event} (gateway emülasyonu: platformun micro-op'u olay SİLEMEZ, eventValue: null yalnız değersiz ölü kayıt bırakır — bu op şemayı okuyup olayı diziden çıkarır ve sayfayı tam gövdeyle geri yazar) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler) | resize_component {target, size?: {w,h}, appearance?: {widthType,widthString,heightType,heightString}, states?: {isWrapper,isScrollable,isSticky,isDroppable,isFloating,isQuickStack}, breakpoint?: 1024} (gateway emülasyonu: platformun micro-op'unda boyut YOKTUR ve move_component'e konan size sessizce düşer — bu op şemayı okuyup hedefin boyutunu değiştirir ve sayfayı tam gövdeyle geri yazar. 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 — kurulmuş bir düğümün isWrapper bayrağını değiştirmenin tek yolu buydu). set_prop_type {target, prop, propType: 'static'|'code'|'translation', ref?, data?} (gateway emülasyonu: replace_prop
|
|
54
|
+
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: 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) | set_event {target, event, eventValue} | remove_event {target, event} (gateway emülasyonu: platformun micro-op'u olay SİLEMEZ, eventValue: null yalnız değersiz ölü kayıt bırakır — bu op şemayı okuyup olayı diziden çıkarır ve sayfayı tam gövdeyle geri yazar) | 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 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 emülasyonu: platformun micro-op'unda boyut YOKTUR ve move_component'e konan size sessizce düşer — bu op şemayı okuyup hedefin boyutunu değiştirir ve sayfayı tam gövdeyle geri yazar. 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 — kurulmuş bir düğümün isWrapper bayrağını değiştirmenin tek yolu buydu). set_prop_type {target, prop, propType: 'static'|'code'|'translation', ref?, data?} (gateway emülasyonu: replace_prop sarmalıyla aynı sonucu verir ve mevcut data'yı tasarım yedeği olarak taşır. Bu op şemayı okuyup prop'un TİPİNİ yerinde değiştirir ve sayfayı tam gövdeyle geri yazar. 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). target = elementId.");
|
|
55
55
|
/**
|
|
56
56
|
* PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
|
|
57
57
|
* komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
|
|
@@ -72,6 +72,20 @@ function unknownEventOps(ops, typesByElementId, eventsBySlug) {
|
|
|
72
72
|
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.`];
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Ön kontroller eskiden İLK hatada fırlatıyordu: dört op'u yanlış olan bir yama dört ayrı turda
|
|
77
|
+
* ancak görülüyordu ve her tur tüm planı yeniden gönderiyordu (S-11, 2026-09-15). Artık op başına
|
|
78
|
+
* toplanır ve tek yanıtta döner. Yama yine ATOMİKTİR — birbirine bağlı op'lar (add_component +
|
|
79
|
+
* set_event) yarım kalmasın diye geçenler de gönderilmez.
|
|
80
|
+
*/
|
|
81
|
+
function rejectedOpsMessage(unordered) {
|
|
82
|
+
const rejected = [...unordered].sort((left, right) => left.index - right.index);
|
|
83
|
+
const lines = rejected.map(entry => `#${entry.index} ${entry.op}${entry.target ? ` '${entry.target}'` : ''} [${entry.code}]: ${entry.message}`);
|
|
84
|
+
const validationHint = rejected.some(entry => entry.code === 'PAGE-016')
|
|
85
|
+
? " 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 }."
|
|
86
|
+
: '';
|
|
87
|
+
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)}`;
|
|
88
|
+
}
|
|
75
89
|
function collectTypes(nodes, into) {
|
|
76
90
|
for (const node of nodes ?? []) {
|
|
77
91
|
if (typeof node.elementId === 'string' && typeof node.type === 'string')
|
|
@@ -79,11 +93,11 @@ function collectTypes(nodes, into) {
|
|
|
79
93
|
collectTypes(node.children, into);
|
|
80
94
|
}
|
|
81
95
|
}
|
|
82
|
-
function removeNodeFromTree(nodes, target) {
|
|
96
|
+
function removeNodeFromTree(nodes, target, parent) {
|
|
83
97
|
for (let index = 0; index < nodes.length; index++) {
|
|
84
98
|
if (nodes[index].elementId === target)
|
|
85
|
-
return nodes.splice(index, 1)[0];
|
|
86
|
-
const found = removeNodeFromTree(nodes[index].children ?? [], target);
|
|
99
|
+
return { node: nodes.splice(index, 1)[0], parent };
|
|
100
|
+
const found = removeNodeFromTree(nodes[index].children ?? [], target, nodes[index]);
|
|
87
101
|
if (found)
|
|
88
102
|
return found;
|
|
89
103
|
}
|
|
@@ -99,48 +113,67 @@ function setDesktopPosition(node, position) {
|
|
|
99
113
|
* PAGE-010 boşluk analizi patch'in SONUCUNU görmek zorunda: tek başına op'lardaki y değerleri,
|
|
100
114
|
* kapsayıcının mevcut çocukları bilinmeden anlamsız. Op'lar okunan şemanın kopyasına uygulanır
|
|
101
115
|
* (add: parent'ın children'ına ekle — kökler retargetRootAdds ile page_container'a gittiği için
|
|
102
|
-
* burada da öyle çözülür; move: düğümü taşı
|
|
116
|
+
* burada da öyle çözülür; move: düğümü taşı — parent verilmemişse aynı ebeveynde kalır — ve verilen
|
|
117
|
+
* position'ı desktop block'una yaz; ikisinde de index platformdaki gibi diziye yerleşir) ve analiz
|
|
103
118
|
* yalnız dokunulan kapsayıcılarda koşar.
|
|
104
119
|
*/
|
|
105
120
|
function simulateLayoutOps(schema, ops) {
|
|
106
121
|
const touched = new Set();
|
|
107
|
-
const attach = (node, requestedParent) => {
|
|
122
|
+
const attach = (node, requestedParent, index) => {
|
|
108
123
|
const parentId = typeof requestedParent === 'string' && requestedParent !== '' && requestedParent !== 'root'
|
|
109
124
|
? requestedParent
|
|
110
125
|
: PAGE_CONTAINER_ELEMENT_ID;
|
|
111
126
|
const parent = findNode(schema, parentId);
|
|
127
|
+
const siblings = parent ? [...(parent.children ?? [])] : schema;
|
|
128
|
+
const insertAt = typeof index === 'number' && Number.isFinite(index) ? Math.max(0, Math.min(index, siblings.length)) : siblings.length;
|
|
129
|
+
siblings.splice(insertAt, 0, node);
|
|
112
130
|
if (parent) {
|
|
113
|
-
parent.children =
|
|
131
|
+
parent.children = siblings;
|
|
114
132
|
touched.add(String(parent.elementId ?? parentId));
|
|
115
133
|
}
|
|
116
134
|
else {
|
|
117
|
-
schema.push(node);
|
|
118
135
|
touched.add('kök');
|
|
119
136
|
}
|
|
120
137
|
};
|
|
121
138
|
for (const op of ops) {
|
|
122
139
|
if (op.op === 'add_component' && op.component && typeof op.component === 'object') {
|
|
123
|
-
attach(JSON.parse(JSON.stringify(op.component)), op.parent);
|
|
140
|
+
attach(JSON.parse(JSON.stringify(op.component)), op.parent, op.index);
|
|
124
141
|
}
|
|
125
142
|
// Düzen prop'ları (layout/flex grubu) simülasyona girer: PAGE-010 flex kapsayıcının içini
|
|
126
143
|
// analiz etmez, PAGE-015 ise op'tan SONRAKİ layout değerine bakar — ikisi de op uygulanmadan
|
|
127
144
|
// doğru karar veremez.
|
|
128
145
|
if (op.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(op.prop ?? ''))) {
|
|
129
146
|
const target = findNode(schema, String(op.target ?? ''));
|
|
147
|
+
const codeWrapper = op.value && typeof op.value === 'object' && op.value.type === 'code';
|
|
130
148
|
if (target)
|
|
131
|
-
target.props = { ...(target.props ?? {}), [String(op.prop)]: { type: 'static', ref: null, data: op.value } };
|
|
149
|
+
target.props = { ...(target.props ?? {}), [String(op.prop)]: codeWrapper ? op.value : { type: 'static', ref: null, data: op.value } };
|
|
132
150
|
}
|
|
133
151
|
if (op.op === 'move_component') {
|
|
134
152
|
const moved = removeNodeFromTree(schema, String(op.target ?? ''));
|
|
135
153
|
if (!moved)
|
|
136
154
|
continue;
|
|
137
155
|
if (op.position && typeof op.position === 'object')
|
|
138
|
-
setDesktopPosition(moved, op.position);
|
|
139
|
-
attach(moved, op.parent);
|
|
156
|
+
setDesktopPosition(moved.node, op.position);
|
|
157
|
+
attach(moved.node, op.parent ?? moved.parent?.elementId ?? 'root', op.index);
|
|
140
158
|
}
|
|
141
159
|
}
|
|
142
160
|
return touched;
|
|
143
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* G-10: `index` taşıyan add/move op'u varsa sayfa okunur ve `flexIndexOrder` flex kapta görünen
|
|
164
|
+
* sırayı konumlara çevirir. Okuma başarısız olursa op'lar olduğu gibi gider — bugünkü davranış.
|
|
165
|
+
*/
|
|
166
|
+
async function orderByIndex(ops, readPage) {
|
|
167
|
+
if (!ops.some(entry => (entry.op === 'add_component' || entry.op === 'move_component') && typeof entry.index === 'number')) {
|
|
168
|
+
return { ops, moved: [], warnings: [] };
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
return flexIndexOrder((await readPage()).schema, ops);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return { ops, moved: [], warnings: [] };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
144
177
|
async function fetchPage(platform, project, environment, applicationId, pageId) {
|
|
145
178
|
const response = await platform.tenant('GET', project, `/pages/${applicationId}/${pageId}`, { environment });
|
|
146
179
|
return response?.data ?? {};
|
|
@@ -429,11 +462,12 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
429
462
|
layoutReport: z.boolean().optional().describe("true: kareyle birlikte her runtime düğümünün efektif piksel kutusunu ve özetini (taşan / viewport dışı / sıfır boyutlu elementId'ler) döner — taşma ve çakışmayı görüntüsüz, sayısal yakalar. Düğümler data-element-id taşımıyorsa layoutReportUnavailable döner."),
|
|
430
463
|
cookies: z.record(z.string()).optional().describe("Tarayıcıya enjekte edilecek EK çerezler (ad → değer): uygulamanın kendi oturum çerezi buraya verilir. Aynı ada sahip çerez gateway jetonunun yerine geçer. anonymous ile birlikte verilemez."),
|
|
431
464
|
actions: z.array(z.object({
|
|
432
|
-
action: z.enum(['set', 'click', 'wait']),
|
|
433
|
-
target: z.string().min(1).optional().describe("Hedefin elementId'si (page_schema_get); runtime düğümü data-element-id ile bulunur"),
|
|
434
|
-
|
|
465
|
+
action: z.enum(['set', 'click', 'wait', 'drag']),
|
|
466
|
+
target: z.string().min(1).optional().describe("Hedefin elementId'si (page_schema_get); runtime düğümü data-element-id ile bulunur. 'drag' için sürüklenen öğe (ör. 'kb_board__card-42', 'cal_plan__event-7')"),
|
|
467
|
+
to: z.string().min(1).optional().describe("Yalnız 'drag': bırakma hedefinin elementId'si — kanban kolonu '<elementId>__col-<value>', takvim günü '<elementId>__day-<yyyy-MM-dd>', tüm gün hücresi '<elementId>__allday-<yyyy-MM-dd>', saat dilimi '<elementId>__slot-<yyyy-MM-dd>T<HH>'"),
|
|
468
|
+
value: z.unknown().optional().describe("'set' için yazılacak değer. Tarih kutularına ISO yazın ('2026-08-01'); çok alanlı tarih aralığında kök DEĞİL kutu hedeflenir: '<elementId>__start' / '<elementId>__end'"),
|
|
435
469
|
ms: z.number().int().min(0).max(15000).optional().describe("'wait' için bekleme süresi")
|
|
436
|
-
})).max(20).optional().describe("Kare ALINMADAN ÖNCE sayfada koşturulacak etkileşim adımları: [{action:'set', target:'flt_month', value:'2022-10'}, {action:'click', target:'btn_sign_in'}]. 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez). Her adımdan sonra yerleşme beklenir ve yanıttaki actionsReport hedefin bulunup bulunmadığını ve adımdan sonra KAÇ AĞ İSTEĞİ çıktığını söyler — 'bağ kuruldu' ile 'çalışıyor' arasındaki farkı ancak bu ayırır (G-24)."),
|
|
470
|
+
})).max(20).optional().describe("Kare ALINMADAN ÖNCE sayfada koşturulacak etkileşim adımları: [{action:'set', target:'flt_month', value:'2022-10'}, {action:'click', target:'btn_sign_in'}, {action:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}]. 'drag' HTML5 sürükle-bırak olaylarını (dragstart→dragover→drop→dragend) hedefin merkezinde atar; kanban cardMove ve takvim eventMove bununla tetiklenir. 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez); ardından kök düğümün runtime prop'larını yeniden okur: actionsReport.propWritten değerin prop'a ULAŞIP ulaşmadığıdır ve false ise applied da false döner — DOM'a yazılmış ama bileşenin duymadığı değer 'uygulandı' sayılmaz (G-14). Her adımdan sonra yerleşme beklenir ve yanıttaki actionsReport hedefin bulunup bulunmadığını ve adımdan sonra KAÇ AĞ İSTEĞİ çıktığını söyler — 'bağ kuruldu' ile 'çalışıyor' arasındaki farkı ancak bu ayırır (G-24)."),
|
|
437
471
|
language: z.string().min(1).optional().describe("Kareyi BU DİLDE çeker: adrese '?lang=<kod>' eklenir ve runtime dili oradan seçer (öncelik: ?lang → tarayıcıda kayıtlı seçim → tarayıcı dilleri → projenin varsayılanı). Seçim hem çeviriye bağlı prop'ları hem çok dilli alanların satır değerlerini kapsar, yani ikinci dilin karesi için sayfaya dil değiştirici KOYMAK GEREKMEZ — referans tasarımı bozulmadan ikinci dil kanıtlanır. ISO 639-1 iki harfli kod ('tr', 'en'); bölgeli kod ('tr-TR') indirgenir ve indirgeme yanıtta bildirilir. Kod projenin languages listesinde yoksa çağrı REDDEDİLİR: runtime tanımadığı kodu yok sayıp kareyi varsayılan dilde çizerdi ve 'çevrildi' sanılan yanlış bir kare raporlanırdı."),
|
|
438
472
|
colorScheme: z.enum(['light', 'dark']).optional().describe("Kareyi açık ya da koyu temada çeker. Ziyaretçi tema tercihi çerezde değil localStorage'da yaşar ve tema store'u onu sayfa açılırken bir kez okur; gateway tercihi sayfa script'lerinden ÖNCE eker, bu yüzden sayfada 'theme-toggle' komponenti OLMASA DA koyu tema karesi alınabilir. Verilmezse projenin kendi modu çizilir."),
|
|
439
473
|
outPath: z.string().min(1).optional().describe('Verilirse PNG bu yerel yola yazılır ve inline görüntü DÖNMEZ; yalnız özet (yol, boyut) döner — büyük fullPage kareleri için')
|
|
@@ -444,6 +478,10 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
444
478
|
if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
|
|
445
479
|
throw new PlatformApiError("page_screenshot: anonymous:true ile asUser/cookies birlikte verilemez — biri kimliksiz kareyi, diğeri belirli bir kimlikle çekilen kareyi ister. Anonim kare için yalnız anonymous:true, üye kimliğiyle kare için yalnız asUser (ve gerekiyorsa cookies) gönderin.");
|
|
446
480
|
}
|
|
481
|
+
const incompleteDrag = actions?.findIndex(step => step.action === 'drag' && (step.target === undefined || step.to === undefined)) ?? -1;
|
|
482
|
+
if (incompleteDrag >= 0) {
|
|
483
|
+
throw new PlatformApiError(`page_screenshot: actions[${incompleteDrag}] 'drag' adımı hem target (sürüklenen öğe) hem to (bırakma hedefi) ister — ör. {action:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}.`);
|
|
484
|
+
}
|
|
447
485
|
if (!config.studioBaseUrl) {
|
|
448
486
|
throw new PlatformApiError("page_screenshot için STUDIO_BASE_URL tanımlı değil. Studio arayüzünün kök adresini .env'e ekleyin (yerel geliştirmede http://localhost:3000, dağıtımda nginx adresi).");
|
|
449
487
|
}
|
|
@@ -558,6 +596,9 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
558
596
|
...(shot.actions?.some(outcome => !outcome.found)
|
|
559
597
|
? { actionsNote: `Bulunamayan hedef var: ${shot.actions.filter(outcome => !outcome.found).map(outcome => outcome.target ?? '?').join(' · ')}. elementId'leri page_schema_get ile doğrulayın; runtime düğümü data-element-id ile bulunuyor.` }
|
|
560
598
|
: {}),
|
|
599
|
+
...(shot.actions?.some(outcome => outcome.propWritten === false)
|
|
600
|
+
? { actionsNote: `Değer DOM'a yazıldı ama runtime prop'una ULAŞMADI: ${shot.actions.filter(outcome => outcome.propWritten === false).map(outcome => `${outcome.action} '${outcome.target ?? '?'}'`).join(' · ')}. Kare dolu kutu gösterse de akış bu değeri okumaz — sonucu doğrulanmış saymayın; adımın note alanına bakın.` }
|
|
601
|
+
: {}),
|
|
561
602
|
// 'wait' bir eylem değil bekleyiştir: akış sayacı ona bağlanmaz, notu tetiklemez (33/S-1).
|
|
562
603
|
...(shot.actions?.some(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0)
|
|
563
604
|
? { actionsNote: `Uygulandı ama HİÇ AĞ İSTEĞİ çıkmadı ve HİÇBİR AKIŞ KOŞMADI: ${shot.actions.filter(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0).map(outcome => `${outcome.action} '${outcome.target ?? '?'}'`).join(' · ')}. Olay bağı şemada görünse bile akış koşmamış olabilir — bağın gerçekten kurulu olduğunu page_runtime_get ile, akışın gövdesini flow_get ile doğrulayın.` }
|
|
@@ -646,22 +687,29 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
646
687
|
// kuru koşu ile yazma AYNI kararı vermeli (P0-2 Step 3).
|
|
647
688
|
const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
|
|
648
689
|
const normalized = normalizeSchemaOps(opRecords);
|
|
649
|
-
const validationIssues =
|
|
690
|
+
const validationIssues = [
|
|
691
|
+
...schemaOpValidationErrors(opRecords).map(issue => `PAGE-016 — ${issue}`),
|
|
692
|
+
...codePropTemplateErrors(opRecords).map(issue => `PAGE-032 — ${issue}`)
|
|
693
|
+
];
|
|
694
|
+
const ordering = await orderByIndex(opRecords, () => fetchPage(platform, project, environment, applicationId, pageId));
|
|
650
695
|
const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
|
|
651
696
|
environment,
|
|
652
|
-
body: { ops:
|
|
697
|
+
body: { ops: ordering.ops }
|
|
653
698
|
})) ?? {});
|
|
654
699
|
if (validationIssues.length > 0)
|
|
655
|
-
response.validationWarnings = validationIssues
|
|
700
|
+
response.validationWarnings = validationIssues;
|
|
701
|
+
if (ordering.warnings.length > 0)
|
|
702
|
+
response.layoutWarnings = ordering.warnings;
|
|
656
703
|
return withNormalization(response, [
|
|
657
704
|
...componentDefaultsNote(normalized.components),
|
|
658
705
|
...validationNote(normalized.validations),
|
|
659
|
-
...replacePropNote(normalized.props)
|
|
706
|
+
...replacePropNote(normalized.props),
|
|
707
|
+
...flexIndexOrderNote(ordering.moved)
|
|
660
708
|
]);
|
|
661
709
|
}));
|
|
662
710
|
server.registerTool('page_schema_patch', {
|
|
663
711
|
title: 'Sayfa şemasına patch uygula',
|
|
664
|
-
description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. expectedUpdatedAt verilirse sayfa o okumadan beri değişmişse istek reddedilir (stale plan koruması). Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler; gövdesi VARSAYILAN OLARAK iskelettir (eklenen düğüm id/type/children'a iner) — tam gövde için view:'full'. Op'un gerçekten kaydedilip kaydedilmediğini gövde değil silentlyDropped söyler.",
|
|
712
|
+
description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. expectedUpdatedAt verilirse sayfa o okumadan beri değişmişse istek reddedilir (stale plan koruması). Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler; gövdesi VARSAYILAN OLARAK iskelettir (eklenen düğüm id/type/children'a iner) — tam gövde için view:'full'. Op'un gerçekten kaydedilip kaydedilmediğini gövde değil silentlyDropped söyler. Gateway ön kontrolleri (PAGE-006/008/016/032, set_prop_type gövdesi, move_component size) op başına toplanır: düşen HER op tek hata yanıtında rejectedOps[] ({ index, op, target, code, message }) ile listelenir ve yama atomik olduğu için hiçbir op gönderilmez — hepsini düzeltip listeyi bir kez yeniden gönderin.",
|
|
665
713
|
inputSchema: {
|
|
666
714
|
project: projectParam,
|
|
667
715
|
environment: environmentParam,
|
|
@@ -680,17 +728,19 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
680
728
|
const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
|
|
681
729
|
const propTypeOps = allOps.filter(entry => entry.op === 'set_prop_type');
|
|
682
730
|
const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
731
|
+
const rejected = [];
|
|
732
|
+
const reject = (code, entry, messages) => {
|
|
733
|
+
for (const message of messages) {
|
|
734
|
+
rejected.push({ index: allOps.indexOf(entry), op: String(entry.op ?? ''), target: typeof entry.target === 'string' ? entry.target : null, code, message });
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
for (const entry of propTypeOps)
|
|
738
|
+
reject('SET-PROP-TYPE', entry, setPropTypeErrors([entry]));
|
|
739
|
+
for (const entry of opRecords.filter(record => record.op === 'move_component' && record.size !== undefined)) {
|
|
740
|
+
reject('MOVE-SIZE', entry, ["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'."]);
|
|
691
741
|
}
|
|
692
|
-
|
|
693
|
-
|
|
742
|
+
for (const entry of opRecords.filter(record => (record.op === 'remove_component' || record.op === 'move_component') && record.target === PAGE_CONTAINER_ELEMENT_ID)) {
|
|
743
|
+
reject('PAGE-006', entry, [`'${PAGE_CONTAINER_ELEMENT_ID}' sayfanın kök kapsayıcısıdır; silinmez ve taşınmaz — silinirse sayfa iskeleti bozulur ve Studio komponentleri kapsayıcı dışında bırakır. Komponent kaldırmak için remove_component'i kapsayıcının İÇİNDEKİ elementId'lerle kullanın (page_schema_get ile listeleyin).`]);
|
|
694
744
|
}
|
|
695
745
|
let cachedPage;
|
|
696
746
|
const readPage = async () => {
|
|
@@ -705,19 +755,10 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
705
755
|
}
|
|
706
756
|
}
|
|
707
757
|
const normalized = normalizeSchemaOps(opRecords);
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
const wantsRootAdd = opRecords.some(entry => entry.op === 'add_component' && (entry.parent === undefined || entry.parent === null || entry.parent === 'root'));
|
|
714
|
-
let retargeted = [];
|
|
715
|
-
if (wantsRootAdd) {
|
|
716
|
-
const page = await readPage();
|
|
717
|
-
if ((page.schema ?? []).some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID)) {
|
|
718
|
-
retargeted = retargetRootAdds(opRecords, PAGE_CONTAINER_ELEMENT_ID);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
758
|
+
for (const entry of opRecords)
|
|
759
|
+
reject('PAGE-016', entry, schemaOpValidationErrors([entry]));
|
|
760
|
+
for (const entry of opRecords)
|
|
761
|
+
reject('PAGE-032', entry, codePropTemplateErrors([entry]));
|
|
721
762
|
if (opRecords.some(entry => entry.op === 'set_event')) {
|
|
722
763
|
const typesByElementId = new Map();
|
|
723
764
|
try {
|
|
@@ -728,10 +769,21 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
728
769
|
}
|
|
729
770
|
collectTypes(opRecords.map(entry => entry.component).filter(Boolean), typesByElementId);
|
|
730
771
|
const eventsBySlug = await fetchComponentEventNames(platform, project, environment, applicationId).catch(() => new Map());
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
throw new PlatformApiError(`PAGE-008 — geçersiz olay adı (istek gönderilmedi): ${unknown.join(' · ')}`, 400);
|
|
772
|
+
for (const entry of opRecords)
|
|
773
|
+
reject('PAGE-008', entry, unknownEventOps([entry], typesByElementId, eventsBySlug));
|
|
734
774
|
}
|
|
775
|
+
if (rejected.length > 0)
|
|
776
|
+
throw new PlatformApiError(rejectedOpsMessage(rejected), 400);
|
|
777
|
+
const wantsRootAdd = opRecords.some(entry => entry.op === 'add_component' && (entry.parent === undefined || entry.parent === null || entry.parent === 'root'));
|
|
778
|
+
let retargeted = [];
|
|
779
|
+
if (wantsRootAdd) {
|
|
780
|
+
const page = await readPage();
|
|
781
|
+
if ((page.schema ?? []).some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID)) {
|
|
782
|
+
retargeted = retargetRootAdds(opRecords, PAGE_CONTAINER_ELEMENT_ID);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const ordering = await orderByIndex(opRecords, readPage);
|
|
786
|
+
const sentOps = ordering.ops;
|
|
735
787
|
// Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
|
|
736
788
|
// silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
|
|
737
789
|
let deletedFlows = [];
|
|
@@ -755,6 +807,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
755
807
|
const formWarnings = [...formBindingWarnings(opRecords), ...formSubmitButtonWarnings(opRecords)];
|
|
756
808
|
let flowWarnings = [];
|
|
757
809
|
let layoutWarnings = [];
|
|
810
|
+
let flowsBeforePatch;
|
|
758
811
|
if (opRecords.some(entry => entry.op === 'remove_component')) {
|
|
759
812
|
try {
|
|
760
813
|
const page = await readPage();
|
|
@@ -768,7 +821,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
768
821
|
try {
|
|
769
822
|
const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
|
|
770
823
|
const page = await readPage();
|
|
771
|
-
|
|
824
|
+
flowsBeforePatch = hosted?.data ?? [];
|
|
825
|
+
flowWarnings = [...flowWarnings, ...staleSetterWarnings(flowsBeforePatch, opRecords, page.schema)];
|
|
772
826
|
}
|
|
773
827
|
catch {
|
|
774
828
|
// akış listesi okunamadı
|
|
@@ -781,7 +835,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
781
835
|
try {
|
|
782
836
|
const page = await readPage();
|
|
783
837
|
const simulated = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
784
|
-
const touched = simulateLayoutOps(simulated,
|
|
838
|
+
const touched = simulateLayoutOps(simulated, sentOps);
|
|
785
839
|
// PAGE-013 op'lar UYGULANDIKTAN SONRAKİ ağaca bakar: tek başına eklenen düğüm masum
|
|
786
840
|
// görünür, iç içeliği ancak hedef parent zinciri belli olunca anlarsın.
|
|
787
841
|
nestedAfterOps = nestedRepeaterNodes(simulated);
|
|
@@ -790,7 +844,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
790
844
|
...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
|
|
791
845
|
...stringStyleWarning(stringStyleNodes(simulated)),
|
|
792
846
|
...inertFlexPropWarning(inertFlexPropNodes(simulated)),
|
|
793
|
-
...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')
|
|
847
|
+
...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'),
|
|
848
|
+
...spacingLayerWarnings(simulated, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')
|
|
794
849
|
];
|
|
795
850
|
}
|
|
796
851
|
catch {
|
|
@@ -814,8 +869,9 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
814
869
|
: [];
|
|
815
870
|
if (propTypeOps.length > 0)
|
|
816
871
|
cachedPage = undefined;
|
|
817
|
-
|
|
818
|
-
|
|
872
|
+
layoutWarnings = [...ordering.warnings, ...layoutWarnings];
|
|
873
|
+
const response = sentOps.length > 0
|
|
874
|
+
? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: sentOps } })) ?? {})
|
|
819
875
|
: { valid: true };
|
|
820
876
|
if (view !== 'full')
|
|
821
877
|
outlineWriteResponse(response);
|
|
@@ -833,12 +889,25 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
833
889
|
response.formWarnings = formWarnings;
|
|
834
890
|
if (flowWarnings.length > 0)
|
|
835
891
|
response.flowWarnings = flowWarnings;
|
|
892
|
+
// FLOW-018 yazma sonrası (G-04): uyarı yamadan önce "kopabilir" der; kopup kopmadığını ancak
|
|
893
|
+
// akışları yeniden okumak söyler. Okuma başarısız olursa yama yine geçerlidir.
|
|
894
|
+
if (flowsBeforePatch && flowWarnings.length > 0 && opRecords.some(entry => entry.op === 'add_component')) {
|
|
895
|
+
try {
|
|
896
|
+
const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
|
|
897
|
+
const lost = lostSetterTargets(flowsBeforePatch, hosted?.data ?? [], opRecords);
|
|
898
|
+
if (lost.length > 0)
|
|
899
|
+
response.flowTargetsLost = lost;
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
// akış listesi yeniden okunamadı
|
|
903
|
+
}
|
|
904
|
+
}
|
|
836
905
|
if (layoutWarnings.length > 0)
|
|
837
906
|
response.layoutWarnings = layoutWarnings;
|
|
838
907
|
// G-23 micro-op ayağı: yama 200 döner ve `appliedPatch` op'un ŞEMAYA yazıldığını söyler, ama
|
|
839
908
|
// alanın gerçekten KAYDEDİLDİĞİNİ söylemez. Yamadan sonra şemayı bir kez okuyup op'un yazmak
|
|
840
909
|
// istediğiyle kayıtlı olanı karşılaştırıyoruz. Okuma başarısız olursa yama yine geçerlidir.
|
|
841
|
-
const contentOps =
|
|
910
|
+
const contentOps = sentOps.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
|
|
842
911
|
let opDrops = [];
|
|
843
912
|
if (contentOps.length > 0) {
|
|
844
913
|
try {
|
|
@@ -854,7 +923,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
854
923
|
...componentDefaultsNote(normalized.components),
|
|
855
924
|
...validationNote(normalized.validations),
|
|
856
925
|
...catalogNotes,
|
|
857
|
-
...replacePropNote(normalized.props)
|
|
926
|
+
...replacePropNote(normalized.props),
|
|
927
|
+
...flexIndexOrderNote(ordering.moved)
|
|
858
928
|
]), opDrops);
|
|
859
929
|
}));
|
|
860
930
|
server.registerTool('page_create', {
|
|
@@ -914,7 +984,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
914
984
|
saved.schema = saved.schema.map(node => summarize(node));
|
|
915
985
|
}
|
|
916
986
|
const normalized = withWriteDiff(withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), drops);
|
|
917
|
-
const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema)), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
|
|
987
|
+
const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema)), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'), ...spacingLayerWarnings(container.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')];
|
|
918
988
|
if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
|
|
919
989
|
return normalized;
|
|
920
990
|
return { ...normalized, layoutWarnings: createWarnings };
|
|
@@ -948,7 +1018,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
|
948
1018
|
const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
|
|
949
1019
|
const gaps = overrides.schema === undefined
|
|
950
1020
|
? []
|
|
951
|
-
: [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema)), ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
|
|
1021
|
+
: [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema)), ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'), ...spacingLayerWarnings(overrides.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')];
|
|
952
1022
|
const body = pageUpdateBody(current, overrides);
|
|
953
1023
|
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
954
1024
|
const summary = {
|