@appaflytech/zeyrek-ai-mcp 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +27 -21
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/componentCatalog.js +122 -27
  4. package/dist/knowledge/componentEvents.js +35 -4
  5. package/dist/knowledge/errorPath.js +1 -1
  6. package/dist/knowledge/normalize.js +15 -0
  7. package/dist/knowledge/rules.js +3 -1
  8. package/dist/knowledge/styleResolve.js +46 -4
  9. package/dist/knowledge/themeResolve.js +936 -0
  10. package/dist/knowledge/themeRules.js +221 -0
  11. package/dist/knowledge/themeSchema.js +335 -0
  12. package/dist/platform/domSummary.js +144 -0
  13. package/dist/platform/screenshot.js +414 -23
  14. package/dist/platform/screenshotMemory.js +55 -0
  15. package/dist/resources/examples/layout-app-shell.json +1 -1
  16. package/dist/resources/examples/theme-brand.json +162 -0
  17. package/dist/resources/guides/component.md +7 -1
  18. package/dist/resources/guides/getting-started.md +4 -0
  19. package/dist/resources/guides/layout.md +30 -26
  20. package/dist/resources/guides/microservice.md +55 -1
  21. package/dist/resources/guides/page.md +87 -44
  22. package/dist/resources/guides/theme.md +210 -0
  23. package/dist/resources/guides/translation.md +6 -4
  24. package/dist/resources/guides/workflow.md +91 -6
  25. package/dist/resources/schema-fingerprint.json +2 -2
  26. package/dist/server.js +52 -43
  27. package/dist/tools/components.js +8 -0
  28. package/dist/tools/designSchemaPatch.js +152 -0
  29. package/dist/tools/flows.js +15 -18
  30. package/dist/tools/layouts.js +4 -4
  31. package/dist/tools/microservices.js +5 -5
  32. package/dist/tools/pages.js +196 -323
  33. package/dist/tools/projects.js +66 -9
  34. package/dist/tools/schemaOps.js +280 -0
  35. package/dist/tools/screenWrite.js +2 -1
  36. package/dist/tools/screens.js +0 -7
  37. package/dist/tools/themes.js +163 -0
  38. package/dist/tools/workflows.js +1 -1
  39. package/package.json +1 -1
@@ -1,21 +1,24 @@
1
1
  import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive, microOps, isBranchName } from './common.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';
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, 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
- import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, outlineAppliedPatch, findNode, flowPath } from './screens.js';
4
+ import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode, flowPath } from './screens.js';
5
5
  import { fetchComponentEventNames } from './components.js';
6
6
  import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
7
+ import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, planEventRemovals, resizeServerOps, planResizes, planPropTypes, rejectedOpsMessage } from './schemaOps.js';
7
8
  import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
8
9
  import { schemaOpDrops, silentlyDropped, unwrapProps, withWriteDiff } from '../platform/writeDiff.js';
9
10
  import { PlatformApiError } from '../platform/errors.js';
10
11
  import { handle, toolResult, toolError } from '../platform/responses.js';
11
12
  import { StudioScreenshotter } from '../platform/screenshot.js';
13
+ import { diffSummaries, summaryLines, summaryStats } from '../platform/domSummary.js';
14
+ import { captureKey, imageHash, sharedCaptureMemory } from '../platform/screenshotMemory.js';
12
15
  import { summarizeLayout } from '../knowledge/layoutSummary.js';
13
16
  import { normalizeLanguageCode } from '../knowledge/languageCodes.js';
14
17
  import { readProjectLanguages } from '../knowledge/languages.js';
15
18
  import { resolveIdentityToken } from '../platform/tokenVault.js';
16
19
  import { mkdir, writeFile } from 'node:fs/promises';
17
20
  import { dirname, resolve } from 'node:path';
18
- import { applyPropTypesToSchema, setPropTypeErrors } from '../knowledge/propTypes.js';
21
+ import { setPropTypeErrors } from '../knowledge/propTypes.js';
19
22
  import { z } from 'zod';
20
23
  /**
21
24
  * Runtime paketindeki `appFunctions`/`pageFunctions` (UiFunctionRuntimeDto: id, name, parameters,
@@ -35,130 +38,6 @@ function runtimeFunctions(value) {
35
38
  .map(parameter => ({ name: parameter.name, required: parameter.required === true, defaultValue: parameter.defaultValue ?? null }))
36
39
  }));
37
40
  }
38
- /**
39
- * Yazma yanıtındaki şema gövdesinin biçimi (B-55 · S-9). Varsayılan `outline`: gövde ajanın
40
- * aynı turda kendi gönderdiği içeriğin yankısıdır, kanıt ise pointer'da ve `silentlyDropped`ta
41
- * durur. Bayrağı bilmeyen ajan pahalı olanı ödemesin diye kırpma OPT-OUT'tur (aynı karar:
42
- * `DS-004`, `datasource_configuration_options`).
43
- */
44
- const writeViewParam = z.enum(['outline', 'full']).optional().describe("Yanıttaki şema gövdesinin biçimi. outline (VARSAYILAN): komponent ağaçları iskelete iner (id/elementId/name/type/children), appliedPatch her op'un op+path'ini verir — 'yalnız o komponente mi dokunuldu' kanıtı JSON Pointer'dadır, 'kaydedildi mi' kanıtı silentlyDropped'tadır. full: platformun döndürdüğü gövde aynen gelir.");
45
- /**
46
- * `appliedPatch` zarfın altında da gelebilir (`{ data: { appliedPatch } }`), düz de.
47
- */
48
- function outlineWriteResponse(response) {
49
- const inner = response.data;
50
- const container = (inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : response);
51
- if (Array.isArray(container.appliedPatch))
52
- container.appliedPatch = outlineAppliedPatch(container.appliedPatch);
53
- }
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
- /**
56
- * PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
57
- * komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
58
- * tipini ve o tipin katalogdaki olay listesini pozitif olarak çözebildiğinde konuşur (layout/widget
59
- * içindeki hedefler ve katalogda olay tanımlamayan tipler doğrulanmadan geçer).
60
- */
61
- function unknownEventOps(ops, typesByElementId, eventsBySlug) {
62
- return ops
63
- .map(op => op)
64
- .filter(op => op.op === 'set_event')
65
- .flatMap(op => {
66
- const target = String(op.target ?? '');
67
- const event = String(op.event ?? '');
68
- const type = typesByElementId.get(target);
69
- const declared = type ? eventsBySlug.get(type) : undefined;
70
- if (!event || !declared || declared.includes(event))
71
- return [];
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
- });
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
- }
89
- function collectTypes(nodes, into) {
90
- for (const node of nodes ?? []) {
91
- if (typeof node.elementId === 'string' && typeof node.type === 'string')
92
- into.set(node.elementId, node.type);
93
- collectTypes(node.children, into);
94
- }
95
- }
96
- function removeNodeFromTree(nodes, target, parent) {
97
- for (let index = 0; index < nodes.length; index++) {
98
- if (nodes[index].elementId === target)
99
- return { node: nodes.splice(index, 1)[0], parent };
100
- const found = removeNodeFromTree(nodes[index].children ?? [], target, nodes[index]);
101
- if (found)
102
- return found;
103
- }
104
- return undefined;
105
- }
106
- function setDesktopPosition(node, position) {
107
- const blocks = Array.isArray(node.blocks) ? node.blocks : [];
108
- const block = blocks.find(entry => Number(entry?.breakpoint) === 1024);
109
- if (block)
110
- block.position = { x: Number(position.x ?? 0), y: Number(position.y ?? 0) };
111
- }
112
- /**
113
- * PAGE-010 boşluk analizi patch'in SONUCUNU görmek zorunda: tek başına op'lardaki y değerleri,
114
- * kapsayıcının mevcut çocukları bilinmeden anlamsız. Op'lar okunan şemanın kopyasına uygulanır
115
- * (add: parent'ın children'ına ekle — kökler retargetRootAdds ile page_container'a gittiği için
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
118
- * yalnız dokunulan kapsayıcılarda koşar.
119
- */
120
- function simulateLayoutOps(schema, ops) {
121
- const touched = new Set();
122
- const attach = (node, requestedParent, index) => {
123
- const parentId = typeof requestedParent === 'string' && requestedParent !== '' && requestedParent !== 'root'
124
- ? requestedParent
125
- : PAGE_CONTAINER_ELEMENT_ID;
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);
130
- if (parent) {
131
- parent.children = siblings;
132
- touched.add(String(parent.elementId ?? parentId));
133
- }
134
- else {
135
- touched.add('kök');
136
- }
137
- };
138
- for (const op of ops) {
139
- if (op.op === 'add_component' && op.component && typeof op.component === 'object') {
140
- attach(JSON.parse(JSON.stringify(op.component)), op.parent, op.index);
141
- }
142
- // Düzen prop'ları (layout/flex grubu) simülasyona girer: PAGE-010 flex kapsayıcının içini
143
- // analiz etmez, PAGE-015 ise op'tan SONRAKİ layout değerine bakar — ikisi de op uygulanmadan
144
- // doğru karar veremez.
145
- if (op.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(op.prop ?? ''))) {
146
- const target = findNode(schema, String(op.target ?? ''));
147
- const codeWrapper = op.value && typeof op.value === 'object' && op.value.type === 'code';
148
- if (target)
149
- target.props = { ...(target.props ?? {}), [String(op.prop)]: codeWrapper ? op.value : { type: 'static', ref: null, data: op.value } };
150
- }
151
- if (op.op === 'move_component') {
152
- const moved = removeNodeFromTree(schema, String(op.target ?? ''));
153
- if (!moved)
154
- continue;
155
- if (op.position && typeof op.position === 'object')
156
- setDesktopPosition(moved.node, op.position);
157
- attach(moved.node, op.parent ?? moved.parent?.elementId ?? 'root', op.index);
158
- }
159
- }
160
- return touched;
161
- }
162
41
  /**
163
42
  * G-10: `index` taşıyan add/move op'u varsa sayfa okunur ve `flexIndexOrder` flex kapta görünen
164
43
  * sırayı konumlara çevirir. Okuma başarısız olursa op'lar olduğu gibi gider — bugünkü davranış.
@@ -179,151 +58,16 @@ async function fetchPage(platform, project, environment, applicationId, pageId)
179
58
  return response?.data ?? {};
180
59
  }
181
60
  /**
182
- * F-8 — platformun `set_event` micro-op'u olay KALDIRAMAZ: `eventValue: null` verildiğinde
183
- * PageSchemaMicroOpApplier kaydı `{ name: 'click' }` olarak (değersiz) bırakır, diziden çıkarmaz;
184
- * ölü bağ şemada artık olarak kalır. Kaldırma yolu tek: şemayı okuyup diziden çıkarmak ve sayfayı
185
- * tam gövdeyle geri yazmak (page_update'in kullandığı PUT — pageUpdateBody her alanı korur).
186
- * `target: 'page'` sayfa düzeyi olayları hedefler.
61
+ * Platformun sayfa micro-op sözlüğü (`PageSchemaMicroOpApplier.KnownOps`). Gateway'in sunucuya
62
+ * ilettiği op'lar yalnız bunlardır; `test/page-micro-op-parity.test.ts` listeyi backend ve
63
+ * `@ng-apps/screen-builder` ile karşılaştırır. `resize_component` gateway adıdır ve sunucuya
64
+ * `set_block` + `set_appearance` + `set_states` olarak çevrilir.
187
65
  */
188
- async function applyEventRemovals(platform, project, environment, applicationId, pageId, page, ops) {
189
- const schema = JSON.parse(JSON.stringify(page.schema ?? []));
190
- const pageEvents = Array.isArray(page.events) ? [...page.events] : [];
191
- const removed = [];
192
- let pageEventsTouched = false;
193
- for (const op of ops) {
194
- const target = String(op.target ?? '');
195
- const event = String(op.event ?? '');
196
- if (!target || !event)
197
- throw new PlatformApiError("remove_event için 'target' ve 'event' zorunludur.", 400);
198
- if (target === 'page') {
199
- const before = pageEvents.length;
200
- const kept = pageEvents.filter(entry => String(entry?.name ?? '') !== event);
201
- if (kept.length !== before) {
202
- pageEvents.length = 0;
203
- pageEvents.push(...kept);
204
- pageEventsTouched = true;
205
- removed.push(`page.${event}`);
206
- }
207
- continue;
208
- }
209
- const node = findNode(schema, target);
210
- if (!node)
211
- throw new PlatformApiError(`remove_event hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
212
- const events = Array.isArray(node.events) ? node.events : [];
213
- const kept = events.filter(entry => String(entry?.name ?? '') !== event);
214
- if (kept.length !== events.length)
215
- removed.push(`${target}.${event}`);
216
- node.events = kept;
217
- }
218
- if (removed.length === 0)
219
- return [];
220
- const body = pageUpdateBody(page, pageEventsTouched ? { schema, events: pageEvents } : { schema });
221
- await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
222
- return removed;
223
- }
224
- function findParentNode(nodes, target) {
225
- for (const node of nodes) {
226
- if ((node.children ?? []).some(child => child.elementId === target))
227
- return node;
228
- const deeper = findParentNode(node.children ?? [], target);
229
- if (deeper)
230
- return deeper;
231
- }
232
- return undefined;
233
- }
234
- /**
235
- * `resize_component` gateway emülasyonudur: platformun micro-op sözlüğünde boyut YOKTUR
236
- * (`move_component` yalnız `position` uygular, gövdeye konan `size` 200 döner ve sessizce düşer —
237
- * ölçüldü 09-04). Bu yüzden kurulmuş bir sayfada bir kartın yüksekliğini değiştirmenin tek yolu
238
- * düğümü `remove_component` + `add_component` ile yeniden kurmaktı; o da ona yazan akış
239
- * setter'larını sessizce öldürüyordu (FLOW-018). Op şemayı okur, hedefin boyutunu değiştirir ve
240
- * sayfayı tam gövdeyle geri yazar — `remove_event` ile aynı desen.
241
- *
242
- * Izgarada boyut `blocks[].size`, flex'te `appearance` alanlarındadır; ikisi de kabul edilir ve
243
- * ebeveynin düzeni okunanı etkisiz kılıyorsa uyarı üretilir (PAGE-015 ile aynı yaklaşım: reddetme,
244
- * ama sessiz de kalma).
245
- */
246
- /**
247
- * G-25 — `states` bir prop değil düğüm alanı: `replace_prop` yazamıyor, `move_component` yalnız
248
- * konum uyguluyor ve `resize_component` (G-14) boyutla sınırlıydı. Kurulmuş bir düğümün
249
- * `isWrapper` bayrağını değiştirmenin tek yolu düğümü silip yeniden kurmaktı; 09-05 koşusunda
250
- * bunun bedeli 20 setter'ın sessizce kopmasıydı (G-21). Anahtar kümesi beyaz listeyle sınırlı:
251
- * `states` açık uçlu bir sözlük ve uydurma bayrak sessizce kaydedilirdi.
252
- */
253
- const WRITABLE_STATES = new Set(['isWrapper', 'isScrollable', 'isSticky', 'isDroppable', 'isFloating', 'isQuickStack']);
254
- async function applyResizes(platform, project, environment, applicationId, pageId, page, ops) {
255
- const schema = JSON.parse(JSON.stringify(page.schema ?? []));
256
- const applied = [];
257
- const warnings = [];
258
- for (const op of ops) {
259
- const target = String(op.target ?? '');
260
- const size = op.size && typeof op.size === 'object' ? op.size : undefined;
261
- const appearance = op.appearance && typeof op.appearance === 'object' ? op.appearance : undefined;
262
- const states = op.states && typeof op.states === 'object' ? op.states : undefined;
263
- const breakpoint = Number(op.breakpoint ?? 1024);
264
- if (!target)
265
- throw new PlatformApiError("resize_component için 'target' zorunludur.", 400);
266
- if (!size && !appearance && !states) {
267
- throw new PlatformApiError(`resize_component '${target}' için 'size' ({ w, h } — ızgara), 'appearance' ({ widthType, widthString, heightType, heightString } — flex) ya da 'states' ({ isWrapper, isScrollable, isSticky, isDroppable, isFloating, isQuickStack }) verilmelidir; üçü de boş.`, 400);
268
- }
269
- if (states) {
270
- const unknownStates = Object.keys(states).filter(key => !WRITABLE_STATES.has(key));
271
- if (unknownStates.length > 0) {
272
- throw new PlatformApiError(`resize_component '${target}': bilinmeyen states anahtarı (${unknownStates.join(', ')}). Yazılabilir bayraklar: ${[...WRITABLE_STATES].join(', ')}.`, 400);
273
- }
274
- }
275
- const node = findNode(schema, target);
276
- if (!node)
277
- throw new PlatformApiError(`resize_component hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
278
- const parent = findParentNode(schema, target);
279
- const inFlex = parent !== undefined && isFlexContainer(parent);
280
- if (size) {
281
- const blocks = Array.isArray(node.blocks) ? node.blocks : [];
282
- const block = blocks.find(entry => Number(entry?.breakpoint) === breakpoint);
283
- if (!block) {
284
- throw new PlatformApiError(`resize_component '${target}' düğümünde ${breakpoint} breakpoint'li blok yok (var olanlar: ${blocks.map(entry => entry?.breakpoint).join(', ') || 'hiç'}). Farklı bir breakpoint verin ya da düğümü add_component ile blokları kurulu olarak ekleyin.`, 400);
285
- }
286
- const current = (block.size ?? {});
287
- block.size = { w: Number(size.w ?? current.w ?? 0), h: Number(size.h ?? current.h ?? 0) };
288
- applied.push(`${target}.size=${JSON.stringify(block.size)}`);
289
- if (inFlex) {
290
- warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex kapsayıcı; orada yerleşimi 'blocks[].size' DEĞİL appearance (widthType/widthString/heightType/heightString) belirler. Boyut yazıldı ama ekranda etkisi olmayabilir — flex'te ölçü vermek için aynı op'a 'appearance' koyun.`);
291
- }
292
- }
293
- if (states) {
294
- node.states = { ...(node.states ?? {}), ...states };
295
- applied.push(`${target}.states=${JSON.stringify(states)}`);
296
- if (states.isWrapper === true) {
297
- warnings.push(`PAGE-020 (uyarı): '${target}' düğümüne isWrapper:true yazıldı — runtime sarmalayıcı düğüme 'min-height: 80vh' veriyor ve bu, sayfanın altına yüzlerce piksel ölü boşluk ekleyebilir (ölçüldü 09-05: dokuz düğüm ~1600px). style ile ezilemez; gerekmiyorsa isWrapper:false bırakın.`);
298
- }
299
- }
300
- if (appearance) {
301
- node.appearance = { ...(node.appearance ?? {}), ...appearance };
302
- applied.push(`${target}.appearance=${JSON.stringify(appearance)}`);
303
- if (!inFlex && parent !== undefined) {
304
- warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex DEĞİL; ızgarada yerleşimi 'blocks[].size' belirler ve appearance genişlik/yükseklik alanları etkisiz kalabilir. Izgarada ölçü vermek için aynı op'a 'size' koyun.`);
305
- }
306
- }
307
- }
308
- if (applied.length === 0)
309
- return { applied, warnings };
310
- const body = pageUpdateBody(page, { schema });
311
- await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
312
- return { applied, warnings };
313
- }
314
- async function applyPropTypes(platform, project, environment, applicationId, pageId, page, ops) {
315
- const schema = JSON.parse(JSON.stringify(page.schema ?? []));
316
- const { applied, missingTargets } = applyPropTypesToSchema(schema, ops);
317
- if (missingTargets.length > 0) {
318
- throw new PlatformApiError(`set_prop_type hedefi bulunamadı: '${missingTargets[0]}'. page_schema_get ile elementId'leri listeleyin.`, 400);
319
- }
320
- if (applied.length === 0)
321
- return [];
322
- const body = pageUpdateBody(page, { schema });
323
- await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
324
- return applied;
325
- }
326
- export function registerPageTools(server, platform, injectedScreenshotter) {
66
+ export const SERVER_PAGE_OPS = [
67
+ 'replace_prop', 'set_binding', 'set_prop_type', 'add_component', 'remove_component', 'move_component', 'reorder', 'set_block',
68
+ 'set_appearance', 'set_states', 'set_event', 'remove_event', 'set_validations', 'set_state_binding', 'remove_state_binding', 'batch'
69
+ ];
70
+ export function registerPageTools(server, platform, injectedScreenshotter, injectedMemory) {
327
71
  const config = platform.config;
328
72
  const asUserParam = z.string().min(1).optional().describe("Uygulama üyesinin kimliği: istek gateway'in servis jetonu yerine bu kimlikle gider. Jeton HEM Authorization başlığında HEM NG_AccessToken çerezinde taşınır, böylece çerez okuyan middleware de geçer. İKİ ŞEKİL kabul edilir: (1) ham JWT, (2) `tok_…` referansı — uygulamanın giriş ucunu microservice_execute ile çağırdığınızda jeton maskelenir ama yanıtın `tokenRefs` alanında referansı döner; jetonu hiç görmeden onu buraya verin (15 dk geçerli). anonymous ile birlikte verilemez.");
329
73
  /** Sayfa ve uç araçları aynı kimlik sözleşmesini paylaşır (G-20); çözüm tokenVault'ta yaşar. */
@@ -434,12 +178,23 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
434
178
  appFunctions: runtimeFunctions(body.appFunctions),
435
179
  pageFunctions: runtimeFunctions(body.pageFunctions),
436
180
  hasTheme: Boolean(body.project?.theme),
181
+ // Tema ADLARI burada durur: bir düğüme $preset yazmadan önce hangi adların geçerli olduğunu
182
+ // bilmek gerekir ve bunun için ayrı bir çağrı atmak gereksiz (tam kayıt için theme_get).
183
+ ...(theme
184
+ ? {
185
+ theme: {
186
+ presets: Object.keys(theme.presets ?? {}),
187
+ componentStyles: Object.keys(theme.componentStyles ?? {})
188
+ }
189
+ }
190
+ : {}),
437
191
  note: resolveStyles
438
- ? "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f). resolvedStyle = componentStyles → preset → düğüm stili kaskadı (aibox'ta tipli grupların derlemesi TABAN katmandır); styleLayers hangi katmanların konuştuğunu söyler."
192
+ ? "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f). resolvedStyle = componentStyles → preset → düğüm stili kaskadı (aibox'ta tipli grupların derlemesi TABAN katmandır); styleLayers hangi katmanların konuştuğunu söyler — styleLayers.componentPreset ise CSS DEĞİL, düğümün hangi bileşen preseti versiyonundan kurulduğunun işaretidir."
439
193
  : "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f); tam gövde gerekirse view:'full'. Bir düğümün ekranda GERÇEKTEN aldığı stili görmek için resolveStyles:true."
440
194
  };
441
195
  }));
442
196
  const screenshotter = injectedScreenshotter ?? new StudioScreenshotter(config);
197
+ const captureMemory = injectedMemory ?? sharedCaptureMemory;
443
198
  const maxInlinePngBytes = 5 * 1024 * 1024;
444
199
  server.registerTool('page_screenshot', {
445
200
  title: 'Sayfanın ekran görüntüsünü al',
@@ -462,22 +217,38 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
462
217
  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."),
463
218
  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."),
464
219
  actions: z.array(z.object({
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')"),
220
+ action: z.enum(['set', 'click', 'wait', 'drag', 'tab', 'select', 'submit']),
221
+ 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'); 'tab' için sekme GRUBUNUN kendisi ('tabs_settings'), 'select' için açılır kutu, 'submit' için form komponenti"),
467
222
  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'"),
223
+ 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'. 'tab' için sekmenin ANAHTARI ('iot') ya da görünen metni; 'select' için seçeneğin value'su ya da görünen metni"),
469
224
  ms: z.number().int().min(0).max(15000).optional().describe("'wait' için bekleme süresi")
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)."),
225
+ })).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:'tab', target:'tabs_settings', value:'iot'}, {action:'select', target:'dd_city', value:'34'}, {action:'submit', target:'frm_note'}, {action:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}]. 'tab' SEKME SEÇER: sekme grubunun köküne 'click' göndermek DOM sırasındaki İLK sekmeye gider ve ikinci sekme hiç açılamıyordu — tablist köküne click artık reddedilir ve bu fiile yönlendirir. Eşleme önce '<elementId>__tab-<sekme anahtarı>' kimliğiyle, o yoksa görünen metinle yapılır; actionsReport.matchedBy hangisi olduğunu, actionsReport.activeTab adımdan SONRA hangi sekmenin açık olduğunu söyler. Etkin olmayan sekmenin içeriği DOM'da YOKTUR: o sekmedeki bir düğüme yazmadan önce 'tab' adımı gerekir. 'select' açılır kutudan seçer — kutu native alan değildir, değeri yazmak hiçbir şey yapmaz, seçeneğe tıklanması gerekir; liste 3 sn'ye kadar YOKLANIR (sorgudan beslenen kutuda seçenekler geç basılır) ve actionsReport.optionsSeen + waitedMs neyin görüldüğünü söyler. 'submit' formu gönder butonu OLMADAN gönderir (form.requestSubmit); actionsReport.submitVia hangi yolun kullanıldığını yazar. '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)."),
471
226
  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ı."),
472
227
  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."),
228
+ format: z.enum(['png', 'jpeg']).optional().describe("Kare biçimi; varsayılan 'png'. Doğrulama karesi için 'jpeg' + quality 60 bağlamı küçültür (ölçüldü 2026-09-16, aynı sayfa: scale 1'de png 8639 B / jpeg60 8691 B — kazanç yok; scale 0.5'te png 3603 B / jpeg60 3035 B). Asıl kaldıraç ÖLÇEKTİR; renk ve kenar keskinliğinin kendisi inceleniyorsa png'de kalın."),
229
+ quality: z.number().int().min(1).max(100).optional().describe("Yalnız format:'jpeg' ile: 1-100, doğrulama için 60 yeter. format 'png' iken verilirse çağrı reddedilir — PNG kayıpsızdır, kalite ayarı yoktur."),
230
+ scale: z.number().min(0.25).max(1).optional().describe("Karenin RASTER ölçeği (deviceScaleFactor); varsayılan 1. 0.5 verildiğinde sayfa yine 1280 CSS pikseli genişliğinde ÇİZİLİR ama kare 640 piksel çıkar: yerleşim, kırılım noktaları ve hideOn davranışı değişmez, yalnız görüntü küçülür. Dar ekran denemek için scale değil viewport kullanın. Yanıttaki imageSize ölçeğin gerçekten uygulandığını sayıyla gösterir."),
231
+ clip: z.union([
232
+ z.object({ element: z.string().min(1) }),
233
+ z.object({ x: z.number(), y: z.number(), width: z.number().positive(), height: z.number().positive() })
234
+ ]).optional().describe("Kareyi bir bölgeyle sınırlar: { element: '<elementId>' } o düğümün kutusunu, { x, y, width, height } elle verilen dikdörtgeni (CSS pikseli) kırpar — yan sütunu ya da tek kartı görmek için tüm sayfayı çekip elle kırpmaya gerek yok. Kutu katlanmış alanın altındaysa viewport onu kapsayana kadar BÜYÜTÜLÜR ve kutu yeniden okunur; düğüm yoksa ya da 0x0 çizildiyse çağrı boş kare yerine HATA döner."),
235
+ summary: z.union([z.enum(['compact', 'full']), z.literal(false)]).optional().describe("Sayfada NE GÖRÜNDÜĞÜNÜN metin özeti; varsayılan 'compact'. Her KÖK runtime düğümü için tek satır: kimlik (tip) · görünür|GİZLİ|ekran dışı · metnin ilk 80 karakteri · rows=<satır> · tab=<açık sekme> · value=<kutudaki değer> · BOŞ · errors=<sayı>; sonda toast ve hata sayfası mesajları. 'full' bunun yanına yapısal domSummary dizisini ve messages'ı koyar; false hiç toplamaz. 'Kart göründü mü, tablo doldu mu, hata var mı' sorularının cevabı buradadır — o sorular için KAREYE GEREK YOKTUR (image:'none' ile birlikte kullanın). Tip alanı runtime'ın data-component-type basmasına bağlıdır; basmıyorsa tipler yazılmaz ve summaryNote bunu söyler."),
236
+ diff: z.boolean().optional().describe("true: aynı sayfanın ÖNCEKİ çekimine göre ne değiştiğini döner (changes.added / removed / changed / messages) ve karenin bayt bayt aynı olup olmadığını söyler (imageUnchanged). Bellek gateway SÜRECİ içindedir: yeniden başlatılınca silinir ve ilk çağrı changes.firstCapture: true döner; sonraki çağrılar baselineAt taşır. Anahtar adres, kimlik, dil, tema, viewport, fullPage, actions, clip, scale ve biçimdir — asUser karesi gateway karesiyle karşılaştırılmaz."),
237
+ image: z.enum(['inline', 'none', 'ifChanged']).optional().describe("'none': kare HİÇ ÇEKİLMEZ, yanıt yalnız özet ve ölçümlerden oluşur — olgu doğrulamasının (kart göründü mü, tablo kaç satır) maliyeti bir kareden iki-üç kat küçüktür ve model bağlamına görüntü girmez. Varsayılan 'inline'. 'ifChanged': kare çekilir ama ÖNCEKİ çekimle bayt bayt aynıysa yanıta KONULMAZ (imageSkipped: true) — aynı sayfayı yeniden koştururken değişmemiş kareyi ikinci kez okumak bağlamın boşa gitmesidir; diff:true ima edilir. outPath ile 'none' birlikte verilemez (yazılacak kare üretilmiyor). Renk, hizalama, referansa sadakat gibi GÖRSEL sorular için kareyi açık bırakın."),
473
238
  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')
474
239
  },
475
240
  annotations: readOnly
476
- }, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, captureUnready, anonymous, asUser, cookies, layoutReport, actions, outPath, expectFailure, language, colorScheme }) => {
241
+ }, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, captureUnready, anonymous, asUser, cookies, layoutReport, actions, outPath, expectFailure, language, colorScheme, summary, image, format, quality, scale, clip, diff }) => {
477
242
  try {
478
243
  if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
479
244
  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.");
480
245
  }
246
+ if (quality !== undefined && (format ?? 'png') === 'png') {
247
+ throw new PlatformApiError("page_screenshot: quality yalnız format:'jpeg' ile verilir — PNG kayıpsızdır ve kalite ayarı yoktur. Küçültmek için scale (yarı çözünürlük) ya da clip (bölge) kullanın.");
248
+ }
249
+ if (image === 'none' && outPath !== undefined) {
250
+ throw new PlatformApiError("page_screenshot: image:'none' ile outPath birlikte verilemez — biri kareyi hiç üretmemeyi, diğeri onu dosyaya yazmayı ister. Olgu doğrulaması için yalnız image:'none', kareyi diske almak için yalnız outPath gönderin.");
251
+ }
481
252
  const incompleteDrag = actions?.findIndex(step => step.action === 'drag' && (step.target === undefined || step.to === undefined)) ?? -1;
482
253
  if (incompleteDrag >= 0) {
483
254
  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'}.`);
@@ -514,11 +285,12 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
514
285
  const size = viewport ?? { width: 1280, height: 800 };
515
286
  const extraCookies = Object.entries(cookies ?? {}).map(([name, value]) => ({ name, value }));
516
287
  const identity = anonymous === true ? 'anonymous' : asUser === undefined ? 'gateway' : 'asUser';
288
+ const identityToken = anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken();
517
289
  const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
518
290
  const shot = await screenshotter.capture({
519
291
  url,
520
292
  apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
521
- token: anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken(),
293
+ token: identityToken,
522
294
  ...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
523
295
  cookieUrl: config.studioBaseUrl,
524
296
  viewport: size,
@@ -527,8 +299,78 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
527
299
  ...(captureUnready === true ? { captureUnready: true } : {}),
528
300
  ...(layoutReport === true ? { layoutReport: true } : {}),
529
301
  ...(colorScheme === undefined ? {} : { colorScheme }),
302
+ // B-2: `summary:false` + `diff` verildiğinde özet yine toplanır — fark ona dayanır ve
303
+ // toplanmazsa bellek hiç yazılmaz, `ifChanged` sessizce ölü bir özellik olurdu.
304
+ ...(summary === false && !(diff === true || image === 'ifChanged') ? { summary: false } : {}),
305
+ ...(image === 'none' ? { image: 'none' } : {}),
306
+ ...(format === undefined ? {} : { format }),
307
+ ...(quality === undefined ? {} : { quality }),
308
+ ...(scale === undefined ? {} : { scale }),
309
+ ...(clip === undefined ? {} : { clip: clip }),
530
310
  ...(actions === undefined ? {} : { actions: actions })
531
311
  });
312
+ // Fark ÖZETTEN çıkar; `summary:false` verilse de fark için toplanmıştır (yanıta girmez).
313
+ // Anahtar KİMLİĞİ ayırır (K8): 'asUser' etiketi yetmez — iki farklı üyenin jetonu aynı
314
+ // etikete düşüyor ve A'nın sayfasıyla B'ninki karşılaştırılıyordu (B-3). Jeton ve çerez
315
+ // DEĞERLERİ anahtara girer; captureKey sha256'lar, ham değer dışarı sızmaz.
316
+ const wantsDiff = diff === true || image === 'ifChanged';
317
+ const memoryKey = captureKey({
318
+ url,
319
+ identity,
320
+ principal: identityToken,
321
+ cookieValues: extraCookies.map(cookie => `${cookie.name}=${cookie.value}`),
322
+ language: languageChoice?.code,
323
+ colorScheme,
324
+ viewport: size,
325
+ fullPage: fullPage === true,
326
+ actions,
327
+ clip,
328
+ scale,
329
+ format,
330
+ quality
331
+ });
332
+ const previous = wantsDiff ? captureMemory.recall(memoryKey) : undefined;
333
+ const currentHash = shot.png === undefined ? undefined : imageHash(shot.png);
334
+ const changes = !wantsDiff
335
+ ? undefined
336
+ : previous === undefined
337
+ // `baselineAt: null` yanıt budamasında (responses.ts `prune`) düşüyordu: alan yok
338
+ // oluyor, geriye yalnız not kalıyordu ve "ilk kare mi, alan mı eksik" ayırt edilemiyordu.
339
+ ? { firstCapture: true }
340
+ : {
341
+ baselineAt: previous.at,
342
+ ...(currentHash === undefined || previous.imageHash === undefined ? {} : { imageUnchanged: currentHash === previous.imageHash }),
343
+ ...(shot.summary === undefined ? {} : diffSummaries(previous.summary, shot.summary))
344
+ };
345
+ const imageUnchanged = changes !== undefined && 'imageUnchanged' in changes ? changes.imageUnchanged === true : false;
346
+ if (wantsDiff && shot.summary !== undefined) {
347
+ // `image:'none'` çağrısı hash üretmez; DOM hiç değişmediyse öncekini KORU, yoksa bir
348
+ // 'none' çağrısı aradaki taban çizgisini siliyor ve sonraki 'ifChanged' atlayamıyordu.
349
+ const unchangedDom = changes !== undefined && 'changed' in changes && changes.changed?.length === 0 && changes.added?.length === 0 && changes.removed?.length === 0;
350
+ const carried = currentHash ?? (unchangedDom ? previous?.imageHash : undefined);
351
+ captureMemory.remember(memoryKey, { at: new Date().toISOString(), ...(carried === undefined ? {} : { imageHash: carried }), summary: shot.summary });
352
+ }
353
+ // Notlar DİZİDE toplanır: hepsi aynı nesnede `actionsNote` anahtarını yazdığı için
354
+ // sonuncusu öncekileri eziyordu ve en sık durumda (sekme değişimi ağ isteği çıkarmaz)
355
+ // "sekme metinle eşlendi" uyarısı sessizce kayboluyordu (B-1).
356
+ const actionsOf = (match) => (shot.actions ?? []).filter(match);
357
+ const actionNotes = [];
358
+ const missing = actionsOf(outcome => !outcome.found);
359
+ const textMatchedTabs = actionsOf(outcome => outcome.action === 'tab' && outcome.matchedBy === 'text');
360
+ const emptyOptions = actionsOf(outcome => outcome.action === 'select' && outcome.optionsSeen === 0);
361
+ const unwritten = actionsOf(outcome => outcome.propWritten === false);
362
+ // 'wait' bir eylem değil bekleyiştir: akış sayacı ona bağlanmaz, notu tetiklemez (33/S-1).
363
+ const silent = actionsOf(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0);
364
+ if (missing.length > 0)
365
+ actionNotes.push(`Bulunamayan hedef var: ${missing.map(outcome => outcome.target ?? '?').join(' · ')}. elementId'leri page_schema_get ile doğrulayın; runtime düğümü data-element-id ile bulunuyor.`);
366
+ if (textMatchedTabs.length > 0)
367
+ actionNotes.push(`Sekme METİNLE eşlendi (runtime '<elementId>__tab-<sekme anahtarı>' kimliğini basmıyor): ${textMatchedTabs.map(outcome => `'${outcome.target ?? '?'}' → ${outcome.activeTab ?? '?'}`).join(' · ')}. Metin çeviriye bağlıdır; language verilen bir çekimde aynı adım kırılır.`);
368
+ if (emptyOptions.length > 0)
369
+ actionNotes.push(`Açılır kutuda hiç seçenek görülmedi: ${emptyOptions.map(outcome => `'${outcome.target ?? '?'}' (${outcome.waitedMs ?? '?'} ms beklendi)`).join(' · ')}. Adımın note alanı kutunun açılıp açılmadığını söyler: açıldıysa seçenekler bir sorgudan geliyor olabilir (query_preview), açılmadıysa kutu devre dışı ya da salt okunur olabilir.`);
370
+ if (unwritten.length > 0)
371
+ actionNotes.push(`Değer DOM'a yazıldı ama runtime prop'una ULAŞMADI: ${unwritten.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.`);
372
+ if (silent.length > 0)
373
+ actionNotes.push(`Uygulandı ama HİÇ AĞ İSTEĞİ çıkmadı ve HİÇBİR AKIŞ KOŞMADI: ${silent.map(outcome => `${outcome.action} '${outcome.target ?? '?'}'`).join(' · ')}. Sekme değiştirmek gibi saf istemci tarafı adımlarda bu BEKLENENDİR; veri bekleyen bir adımda olay bağı şemada görünse de akış koşmamış olabilir — bağı page_runtime_get, akış gövdesini flow_get ile doğrulayın.`);
532
374
  const meta = {
533
375
  url,
534
376
  environment: resolvedEnvironment,
@@ -539,8 +381,41 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
539
381
  fullPage: fullPage === true,
540
382
  identity,
541
383
  cookieNames,
542
- sizeBytes: shot.png.byteLength,
384
+ ...(shot.png === undefined
385
+ ? { image: 'none' }
386
+ : {
387
+ sizeBytes: shot.png.byteLength,
388
+ format: format ?? 'png',
389
+ ...(scale === undefined ? {} : { scale }),
390
+ ...(shot.imageSize === undefined ? {} : { imageSize: shot.imageSize }),
391
+ ...(shot.clip === undefined ? {} : { clip: shot.clip })
392
+ }),
393
+ ...(shot.clip !== undefined && (layoutReport === true || fullPage === true)
394
+ ? { clipNote: 'clip yalnız GÖRÜNTÜYÜ keser: layoutSummary kutuları, summaryText satırları ve contentHeight sayfanın TAMAMI için ölçülür.' }
395
+ : {}),
543
396
  elapsedMs: shot.elapsedMs,
397
+ // S-5: yanıtın olgu katmanı. Kare açılmadan okunabilsin diye METİN olarak da döner.
398
+ ...(shot.summary === undefined || summary === false
399
+ ? {}
400
+ : {
401
+ summaryText: summaryLines(shot.summary).join('\n'),
402
+ summaryStats: summaryStats(shot.summary),
403
+ ...(summary === 'full' ? { domSummary: shot.summary.nodes, messages: shot.summary.messages } : {}),
404
+ ...(shot.summary.truncated ? { summaryTruncated: true, summaryTruncatedNote: 'Sayfada 300 kök düğümden fazlası var; özet ilk 300 düğümde kesildi. Daha dar bakmak için sayfayı bölerek doğrulayın.' } : {}),
405
+ ...(shot.summary.typed ? {} : { summaryNote: "Runtime data-component-type basmıyor (eski runtime ya da frontend ayağı dağıtılmadı) — satırlarda komponent TİPİ yazılmadı; kimlik ve görünürlük yine doğrudur." })
406
+ }),
407
+ ...(shot.summaryUnavailable === undefined ? {} : { summaryUnavailable: shot.summaryUnavailable }),
408
+ ...(changes === undefined
409
+ ? {}
410
+ : {
411
+ changes,
412
+ ...('firstCapture' in changes
413
+ ? { changesNote: 'Bu adresin ilk karesi — karşılaştırılacak önceki çekim yok. Bellek gateway süreci içindedir ve yeniden başlatılınca silinir.' }
414
+ : {}),
415
+ ...('imageUnchanged' in changes && changes.imageUnchanged === false && 'changed' in changes && changes.changed?.length === 0 && changes.added?.length === 0 && changes.removed?.length === 0
416
+ ? { changesNote: 'Kare bayt bayt değişti ama DOM özeti aynı: fark piksel düzeyinde (yazı tipi yerleşmesi, animasyon, imleç). Yapısal bir değişiklik yok.' }
417
+ : {})
418
+ }),
544
419
  ...(shot.contentHeight === undefined ? {} : { contentHeight: shot.contentHeight }),
545
420
  ...(shot.fullPageTruncated
546
421
  ? { fullPageTruncated: true, note: `Sayfa içeriği ${shot.contentHeight} px — kare 16384 px'de kırpıldı. Alt kısmı görmek için viewport'u kaydırılmış bir bölüme odaklayın ya da sayfayı bölerek doğrulayın.` }
@@ -593,16 +468,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
593
468
  runtimeErrors: shot.runtimeErrors,
594
469
  runtimeErrorsNote: `Sayfa akışı ${shot.runtimeErrors.length} yapısal hata bıraktı; kare 'çalışıyor' görünse bile bu adımlar işini YAPMADI. element/property alanı hangi komponentin hangi prop'unun yazılamadığını söyler — o komponent eski/boş değerindedir. Adımı flow_get ile açıp ifadeyi düzeltin.`
595
470
  }),
596
- ...(shot.actions?.some(outcome => !outcome.found)
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.` }
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
- : {}),
602
- // 'wait' bir eylem değil bekleyiştir: akış sayacı ona bağlanmaz, notu tetiklemez (33/S-1).
603
- ...(shot.actions?.some(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0)
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.` }
605
- : {}),
471
+ ...(actionNotes.length === 0 ? {} : { actionsNote: actionNotes.join('\n') }),
606
472
  ...(shot.layout === undefined
607
473
  ? {}
608
474
  : shot.layout.length === 0
@@ -617,6 +483,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
617
483
  try {
618
484
  // B-6: klasör yoksa ENOENT ile düşüyordu; koşu ajanı her seferinde klasörü elle açıyordu.
619
485
  await mkdir(dirname(absolute), { recursive: true });
486
+ if (shot.png === undefined)
487
+ throw new PlatformApiError('page_screenshot: yazılacak kare üretilmedi (image kipi kareyi atladı) — outPath ile birlikte kare üreten bir kip kullanın.');
620
488
  await writeFile(absolute, shot.png);
621
489
  }
622
490
  catch (error) {
@@ -625,12 +493,19 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
625
493
  }
626
494
  return toolResult({ path: absolute, ...meta }, config);
627
495
  }
496
+ // image:'none' — kare hiç üretilmedi; yanıt yalnız olgu katmanıdır.
497
+ if (shot.png === undefined)
498
+ return toolResult(meta, config);
499
+ // 'ifChanged' — kare çekildi ama öncekiyle AYNI: bağlama ikinci kez girmesin.
500
+ if (image === 'ifChanged' && imageUnchanged) {
501
+ return toolResult({ ...meta, imageSkipped: true, imageSkippedNote: 'Kare öncekiyle bayt bayt aynı olduğu için yanıta konulmadı (image: ifChanged). Yine de görmek için image varsayılanına dönün.' }, config);
502
+ }
628
503
  if (shot.png.byteLength > maxInlinePngBytes) {
629
- throw new PlatformApiError(`Görüntü ${shot.png.byteLength} B — inline sınırı (${maxInlinePngBytes} B) aşıyor. outPath verin (dosyaya yazılır, model bağlamına girmez) ya da fullPage'i kapatıp viewport'u küçültün.`);
504
+ throw new PlatformApiError(`Görüntü ${shot.png.byteLength} B — inline sınırı (${maxInlinePngBytes} B) aşıyor. outPath verin (dosyaya yazılır, model bağlamına girmez), image:'none' ile yalnız özet alın ya da fullPage'i kapatıp viewport'u küçültün.`);
630
505
  }
631
506
  return {
632
507
  content: [
633
- { type: 'image', data: shot.png.toString('base64'), mimeType: 'image/png' },
508
+ { type: 'image', data: shot.png.toString('base64'), mimeType: shot.mimeType ?? 'image/png' },
634
509
  { type: 'text', text: JSON.stringify(meta) }
635
510
  ],
636
511
  structuredContent: meta
@@ -642,7 +517,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
642
517
  });
643
518
  server.registerTool('page_schema_get', {
644
519
  title: 'Sayfa şemasını oku',
645
- description: "Sayfanın komponent şemasını okur; yanıttaki updatedAt değeri page_schema_patch'in expectedUpdatedAt kontrolünde kullanılır.",
520
+ description: "Sayfanın komponent şemasını okur. Yanıttaki version değerini page_schema_patch'e baseVersion olarak verin: sayfa o okumadan beri değiştiyse yama 409 ile reddedilir ve hiçbir op uygulanmaz.",
646
521
  inputSchema: {
647
522
  project: projectParam,
648
523
  environment: environmentParam,
@@ -655,17 +530,18 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
655
530
  }, handle(config, async ({ project, environment, applicationId, pageId, mode, componentId }) => {
656
531
  const page = await fetchPage(platform, project, environment, applicationId, pageId);
657
532
  const updatedAt = page.audit?.updatedAt ?? page.audit?.createdAt;
533
+ const version = page.version;
658
534
  if (mode === 'full')
659
- return { updatedAt, schema: page.schema ?? [] };
535
+ return { version, updatedAt, schema: page.schema ?? [] };
660
536
  if (mode === 'subtree') {
661
537
  if (!componentId)
662
538
  throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
663
539
  const subtree = findNode(page.schema, componentId);
664
540
  if (!subtree)
665
541
  throw new PlatformApiError(`Sayfada '${componentId}' id'li bir komponent yok; page_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
666
- return { updatedAt, schema: subtree };
542
+ return { version, updatedAt, schema: subtree };
667
543
  }
668
- return { updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
544
+ return { version, updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
669
545
  }));
670
546
  server.registerTool('page_state', {
671
547
  title: 'Sayfa durumunu getir',
@@ -683,9 +559,9 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
683
559
  const propTypeIssues = setPropTypeErrors(allOps.filter(entry => entry.op === 'set_prop_type'));
684
560
  if (propTypeIssues.length > 0)
685
561
  throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
686
- // Gateway emülasyonu olan op'lar platformun doğrulama ucunu tanımadığı için gövdeden ayrılır;
687
- // kuru koşu ile yazma AYNI kararı vermeli (P0-2 Step 3).
688
- const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
562
+ // Kuru koşu ile yazma AYNI gövdeyi görür (P0-2 Step 3): resize_component burada da sunucu
563
+ // op'larına çevrilir, platform dokunulan düğümleri aynı kapsamla doğrular.
564
+ const opRecords = allOps.filter(entry => entry.op !== 'resize_component');
689
565
  const normalized = normalizeSchemaOps(opRecords);
690
566
  const validationIssues = [
691
567
  ...schemaOpValidationErrors(opRecords).map(issue => `PAGE-016 — ${issue}`),
@@ -694,7 +570,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
694
570
  const ordering = await orderByIndex(opRecords, () => fetchPage(platform, project, environment, applicationId, pageId));
695
571
  const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
696
572
  environment,
697
- body: { ops: ordering.ops }
573
+ body: { ops: [...allOps.filter(entry => entry.op === 'resize_component').flatMap(resizeServerOps), ...ordering.ops] }
698
574
  })) ?? {});
699
575
  if (validationIssues.length > 0)
700
576
  response.validationWarnings = validationIssues;
@@ -709,7 +585,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
709
585
  }));
710
586
  server.registerTool('page_schema_patch', {
711
587
  title: 'Sayfa şemasına patch uygula',
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.",
588
+ description: "Micro-op'ları doğrulama duvarından geçirip TEK PATCH ile kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. İstek atomiktir: bir op reddedilirse hiçbiri yazılmaz. baseVersion (page_schema_get çıktısındaki version ya da önceki yamanın newVersion'ı) verilirse sayfa o sürümden beri değişmişse 409 döner ve hiçbir op uygulanmaz (stale plan koruması); verilmezse gateway yalnız bu çağrıda zaten okuduğu sayfanın sürümünü gönderir. 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 hiçbir op gönderilmez — hepsini düzeltip listeyi bir kez yeniden gönderin.",
713
589
  inputSchema: {
714
590
  project: projectParam,
715
591
  environment: environmentParam,
@@ -717,12 +593,13 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
717
593
  pageId: guid,
718
594
  ops: opsParam.optional(),
719
595
  operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"),
720
- expectedUpdatedAt: z.string().optional().describe("page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz"),
596
+ baseVersion: z.string().optional().describe("page_schema_get çıktısındaki version ya da önceki page_schema_patch yanıtındaki newVersion; sayfa o sürümden beri değiştiyse 409 döner ve hiçbir op uygulanmaz"),
597
+ expectedUpdatedAt: z.string().optional().describe("Eski koruma: page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz. baseVersion tercih edilir"),
721
598
  allowFlowDeletion: z.boolean().optional().describe("true: bu yama bir akışı referanssız bırakacaksa yine de uygulanır. Varsayılan false — platform referanssız akışı KALICI siler ve gateway isteği önden reddeder (G-21). Silinen akışlar yanıtta deletedFlows ile listelenir."),
722
599
  view: writeViewParam
723
600
  },
724
601
  annotations: updates
725
- }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, expectedUpdatedAt, allowFlowDeletion, view }) => {
602
+ }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, baseVersion, expectedUpdatedAt, allowFlowDeletion, view }) => {
726
603
  const allOps = microOps('page_schema_patch', ops, operations).map(op => op);
727
604
  const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
728
605
  const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
@@ -787,7 +664,9 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
787
664
  // Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
788
665
  // silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
789
666
  let deletedFlows = [];
790
- if (opRecords.some(entry => ['remove_component', 'set_event', 'remove_event'].includes(String(entry.op)))) {
667
+ // remove_event opRecords'ta değil, ayrı listede durur; koşul yalnız opRecords'a baktığı sürece
668
+ // yalnız olay kaldıran bir yama bu korumayı hiç tetiklemiyordu (2026-09-14).
669
+ if ([...opRecords, ...removeEventOps].some(entry => ['remove_component', 'set_event', 'remove_event'].includes(String(entry.op)))) {
791
670
  try {
792
671
  const page = await readPage();
793
672
  deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
@@ -854,31 +733,25 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
854
733
  }
855
734
  if (nestedAfterOps.length > 0)
856
735
  throw new PlatformApiError(nestedRepeaterMessage(nestedAfterOps), 400);
857
- const removedEvents = removeEventOps.length > 0
858
- ? await applyEventRemovals(platform, project, environment, applicationId, pageId, await readPage(), removeEventOps)
859
- : [];
860
- if (removeEventOps.length > 0)
861
- cachedPage = undefined;
862
- const resized = resizeOps.length > 0
863
- ? await applyResizes(platform, project, environment, applicationId, pageId, await readPage(), resizeOps)
864
- : { applied: [], warnings: [] };
865
- if (resizeOps.length > 0)
866
- cachedPage = undefined;
867
- const propTypes = propTypeOps.length > 0
868
- ? await applyPropTypes(platform, project, environment, applicationId, pageId, await readPage(), propTypeOps)
869
- : [];
870
- if (propTypeOps.length > 0)
871
- cachedPage = undefined;
736
+ const removedEvents = planEventRemovals(removeEventOps);
737
+ const resized = resizeOps.length > 0 ? planResizes((await readPage()).schema ?? [], resizeOps) : { serverOps: [], applied: [], warnings: [] };
738
+ const propTypes = propTypeOps.length > 0 ? planPropTypes((await readPage()).schema ?? [], propTypeOps) : { serverOps: [], applied: [] };
739
+ const serverOps = [...removeEventOps, ...resized.serverOps, ...propTypes.serverOps, ...sentOps];
872
740
  layoutWarnings = [...ordering.warnings, ...layoutWarnings];
873
- const response = sentOps.length > 0
874
- ? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: sentOps } })) ?? {})
875
- : { valid: true };
741
+ // Tek istek, tek sürüm: öykünülen op'lar 2026-09-14'e kadar ayrı GET + tam PUT çiftleriydi ve
742
+ // yamadan ÖNCE yazılıyordu — PATCH reddedilse bile kalıcıydılar. Artık hepsi aynı atomik PATCH'te.
743
+ // baseVersion yalnız biliniyorsa gider (ajanın verdiği ya da bu çağrıda zaten okunan sayfanınki):
744
+ // sürümü öğrenmek için ayrı GET atılmaz, platform satırı her yamada kilitler.
745
+ const response = ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, {
746
+ environment,
747
+ body: { ops: serverOps, baseVersion: baseVersion ?? (typeof cachedPage?.version === 'string' ? cachedPage.version : undefined) }
748
+ })) ?? {});
876
749
  if (view !== 'full')
877
750
  outlineWriteResponse(response);
878
751
  if (removedEvents.length > 0)
879
752
  response.removedEvents = removedEvents;
880
- if (propTypes.length > 0)
881
- response.propTypes = propTypes;
753
+ if (propTypes.applied.length > 0)
754
+ response.propTypes = propTypes.applied;
882
755
  if (resized.applied.length > 0)
883
756
  response.resized = resized.applied;
884
757
  if (resized.warnings.length > 0)