@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.
Files changed (35) hide show
  1. package/dist/knowledge/backendConstraints.js +3 -0
  2. package/dist/knowledge/componentCatalog.js +59 -15
  3. package/dist/knowledge/normalize.js +459 -23
  4. package/dist/knowledge/propTypes.js +5 -4
  5. package/dist/knowledge/rules.js +110 -5
  6. package/dist/knowledge/styleResolve.js +6 -1
  7. package/dist/platform/http.js +10 -2
  8. package/dist/platform/screenshot.js +80 -8
  9. package/dist/platform/strictArguments.js +32 -0
  10. package/dist/platform/writeDiff.js +6 -3
  11. package/dist/resources/examples/page-sign-in.json +1 -1
  12. package/dist/resources/guides/component.md +1 -1
  13. package/dist/resources/guides/expressions.md +6 -0
  14. package/dist/resources/guides/getting-started.md +2 -0
  15. package/dist/resources/guides/layout.md +6 -0
  16. package/dist/resources/guides/microservice.md +105 -5
  17. package/dist/resources/guides/page.md +81 -18
  18. package/dist/resources/guides/translation.md +11 -10
  19. package/dist/resources/guides/workflow.md +50 -1
  20. package/dist/resources/schema-fingerprint.json +2 -2
  21. package/dist/resources/schemas/CreateMicroserviceCommand.json +4 -1
  22. package/dist/resources/schemas/CreateWorkflowCommand.json +4 -1
  23. package/dist/resources/schemas/PreviewMicroserviceCommand.json +4 -1
  24. package/dist/resources/schemas/PreviewWorkflowCommand.json +4 -1
  25. package/dist/resources/schemas/StepDto.json +4 -1
  26. package/dist/resources/schemas/UpdateMicroserviceCommand.json +4 -1
  27. package/dist/resources/schemas/UpdateWorkflowCommand.json +4 -1
  28. package/dist/server.js +2 -1
  29. package/dist/tools/flows.js +9 -3
  30. package/dist/tools/layouts.js +4 -4
  31. package/dist/tools/microservices.js +70 -6
  32. package/dist/tools/pages.js +127 -57
  33. package/dist/tools/queries.js +72 -6
  34. package/dist/tools/workflows.js +9 -5
  35. 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
- return withNormalization(await platform.tenantRaw(method, project, executePath('m', application, microservice, routeParams), { environment, query, body, headers: requestHeaders, anonymous, mirrorAuthorizationCookie: true }), notes);
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 payload = await platform.tenantRaw(method, project, executePath('debug', application, microservice, routeParams), { environment, query, body, headers: requestHeaders, anonymous, mirrorAuthorizationCookie: true });
342
- return withNormalization({ ...payload, ...await middlewareChain(platform, project, environment, application, microservice, payload?.httpStatus) }, notes);
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',
@@ -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 gönderilen ham değeri HER ZAMAN static Prop'una sarar ve sarmalanmış Prop nesnesini reddeder — kurulu bir etiketi çeviriye bağlamanın tek yolu düğümü remove_component + add_component ile yeniden kurmaktı ve o yol düğüme yazan setter iteratee'lerini sessizce öldürüyordu. 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.");
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şı ve verilen position'ı desktop block'una yaz) ve analiz
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 = [...(parent.children ?? []), node];
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
- value: z.unknown().optional().describe("'set' için yazılacak değer"),
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 = schemaOpValidationErrors(opRecords);
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: opRecords }
697
+ body: { ops: ordering.ops }
653
698
  })) ?? {});
654
699
  if (validationIssues.length > 0)
655
- response.validationWarnings = validationIssues.map(issue => `PAGE-016 — ${issue}`);
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 propTypeIssues = setPropTypeErrors(propTypeOps);
684
- if (propTypeIssues.length > 0)
685
- throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
686
- const containerOps = opRecords
687
- .filter(entry => (entry.op === 'remove_component' || entry.op === 'move_component') && entry.target === PAGE_CONTAINER_ELEMENT_ID);
688
- const sizedMoves = opRecords.filter(entry => entry.op === 'move_component' && entry.size !== undefined);
689
- if (sizedMoves.length > 0) {
690
- 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);
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
- if (containerOps.length > 0) {
693
- throw new PlatformApiError(`'${PAGE_CONTAINER_ELEMENT_ID}' sayfanın kök kapsayıcısıdır; silinmez ve taşınmaz (PAGE-006) — 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).`, 400);
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 validationIssues = schemaOpValidationErrors(opRecords);
709
- if (validationIssues.length > 0) {
710
- throw new PlatformApiError(`PAGE-016 — validation gövdesi eksik (istek gönderilmedi): ${validationIssues.join(' · ')}. `
711
- + "Ç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);
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 unknown = unknownEventOps(opRecords, typesByElementId, eventsBySlug);
732
- if (unknown.length > 0)
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
- flowWarnings = [...flowWarnings, ...staleSetterWarnings(hosted?.data ?? [], opRecords, page.schema)];
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, opRecords);
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
- const response = opRecords.length > 0
818
- ? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: opRecords } })) ?? {})
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 = opRecords.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
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 = {