@appaflytech/zeyrek-ai-mcp 0.0.7 → 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 (54) hide show
  1. package/README.md +27 -21
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/backendConstraints.js +3 -0
  4. package/dist/knowledge/componentCatalog.js +172 -33
  5. package/dist/knowledge/componentEvents.js +35 -4
  6. package/dist/knowledge/errorPath.js +1 -1
  7. package/dist/knowledge/normalize.js +474 -23
  8. package/dist/knowledge/propTypes.js +5 -4
  9. package/dist/knowledge/rules.js +113 -6
  10. package/dist/knowledge/styleResolve.js +52 -5
  11. package/dist/knowledge/themeResolve.js +936 -0
  12. package/dist/knowledge/themeRules.js +221 -0
  13. package/dist/knowledge/themeSchema.js +335 -0
  14. package/dist/platform/domSummary.js +144 -0
  15. package/dist/platform/http.js +10 -2
  16. package/dist/platform/screenshot.js +492 -29
  17. package/dist/platform/screenshotMemory.js +55 -0
  18. package/dist/platform/strictArguments.js +32 -0
  19. package/dist/platform/writeDiff.js +6 -3
  20. package/dist/resources/examples/layout-app-shell.json +1 -1
  21. package/dist/resources/examples/page-sign-in.json +1 -1
  22. package/dist/resources/examples/theme-brand.json +162 -0
  23. package/dist/resources/guides/component.md +8 -2
  24. package/dist/resources/guides/expressions.md +6 -0
  25. package/dist/resources/guides/getting-started.md +6 -0
  26. package/dist/resources/guides/layout.md +36 -26
  27. package/dist/resources/guides/microservice.md +160 -6
  28. package/dist/resources/guides/page.md +165 -59
  29. package/dist/resources/guides/theme.md +210 -0
  30. package/dist/resources/guides/translation.md +13 -10
  31. package/dist/resources/guides/workflow.md +141 -7
  32. package/dist/resources/schema-fingerprint.json +2 -2
  33. package/dist/resources/schemas/CreateMicroserviceCommand.json +4 -1
  34. package/dist/resources/schemas/CreateWorkflowCommand.json +4 -1
  35. package/dist/resources/schemas/PreviewMicroserviceCommand.json +4 -1
  36. package/dist/resources/schemas/PreviewWorkflowCommand.json +4 -1
  37. package/dist/resources/schemas/StepDto.json +4 -1
  38. package/dist/resources/schemas/UpdateMicroserviceCommand.json +4 -1
  39. package/dist/resources/schemas/UpdateWorkflowCommand.json +4 -1
  40. package/dist/server.js +52 -42
  41. package/dist/tools/components.js +8 -0
  42. package/dist/tools/designSchemaPatch.js +152 -0
  43. package/dist/tools/flows.js +24 -21
  44. package/dist/tools/layouts.js +8 -8
  45. package/dist/tools/microservices.js +74 -10
  46. package/dist/tools/pages.js +275 -332
  47. package/dist/tools/projects.js +66 -9
  48. package/dist/tools/queries.js +72 -6
  49. package/dist/tools/schemaOps.js +280 -0
  50. package/dist/tools/screenWrite.js +2 -1
  51. package/dist/tools/screens.js +0 -7
  52. package/dist/tools/themes.js +163 -0
  53. package/dist/tools/workflows.js +10 -6
  54. 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, 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, 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,
@@ -36,261 +39,35 @@ function runtimeFunctions(value) {
36
39
  }));
37
40
  }
38
41
  /**
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`).
42
+ * G-10: `index` taşıyan add/move op'u varsa sayfa okunur ve `flexIndexOrder` flex kapta görünen
43
+ * sırayı konumlara çevirir. Okuma başarısız olursa op'lar olduğu gibi gider — bugünkü davranış.
43
44
  */
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} | 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.");
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
- function collectTypes(nodes, into) {
76
- for (const node of nodes ?? []) {
77
- if (typeof node.elementId === 'string' && typeof node.type === 'string')
78
- into.set(node.elementId, node.type);
79
- collectTypes(node.children, into);
45
+ async function orderByIndex(ops, readPage) {
46
+ if (!ops.some(entry => (entry.op === 'add_component' || entry.op === 'move_component') && typeof entry.index === 'number')) {
47
+ return { ops, moved: [], warnings: [] };
80
48
  }
81
- }
82
- function removeNodeFromTree(nodes, target) {
83
- for (let index = 0; index < nodes.length; index++) {
84
- if (nodes[index].elementId === target)
85
- return nodes.splice(index, 1)[0];
86
- const found = removeNodeFromTree(nodes[index].children ?? [], target);
87
- if (found)
88
- return found;
49
+ try {
50
+ return flexIndexOrder((await readPage()).schema, ops);
89
51
  }
90
- return undefined;
91
- }
92
- function setDesktopPosition(node, position) {
93
- const blocks = Array.isArray(node.blocks) ? node.blocks : [];
94
- const block = blocks.find(entry => Number(entry?.breakpoint) === 1024);
95
- if (block)
96
- block.position = { x: Number(position.x ?? 0), y: Number(position.y ?? 0) };
97
- }
98
- /**
99
- * PAGE-010 boşluk analizi patch'in SONUCUNU görmek zorunda: tek başına op'lardaki y değerleri,
100
- * kapsayıcının mevcut çocukları bilinmeden anlamsız. Op'lar okunan şemanın kopyasına uygulanır
101
- * (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
103
- * yalnız dokunulan kapsayıcılarda koşar.
104
- */
105
- function simulateLayoutOps(schema, ops) {
106
- const touched = new Set();
107
- const attach = (node, requestedParent) => {
108
- const parentId = typeof requestedParent === 'string' && requestedParent !== '' && requestedParent !== 'root'
109
- ? requestedParent
110
- : PAGE_CONTAINER_ELEMENT_ID;
111
- const parent = findNode(schema, parentId);
112
- if (parent) {
113
- parent.children = [...(parent.children ?? []), node];
114
- touched.add(String(parent.elementId ?? parentId));
115
- }
116
- else {
117
- schema.push(node);
118
- touched.add('kök');
119
- }
120
- };
121
- for (const op of ops) {
122
- if (op.op === 'add_component' && op.component && typeof op.component === 'object') {
123
- attach(JSON.parse(JSON.stringify(op.component)), op.parent);
124
- }
125
- // Düzen prop'ları (layout/flex grubu) simülasyona girer: PAGE-010 flex kapsayıcının içini
126
- // analiz etmez, PAGE-015 ise op'tan SONRAKİ layout değerine bakar — ikisi de op uygulanmadan
127
- // doğru karar veremez.
128
- if (op.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(op.prop ?? ''))) {
129
- const target = findNode(schema, String(op.target ?? ''));
130
- if (target)
131
- target.props = { ...(target.props ?? {}), [String(op.prop)]: { type: 'static', ref: null, data: op.value } };
132
- }
133
- if (op.op === 'move_component') {
134
- const moved = removeNodeFromTree(schema, String(op.target ?? ''));
135
- if (!moved)
136
- continue;
137
- if (op.position && typeof op.position === 'object')
138
- setDesktopPosition(moved, op.position);
139
- attach(moved, op.parent);
140
- }
52
+ catch {
53
+ return { ops, moved: [], warnings: [] };
141
54
  }
142
- return touched;
143
55
  }
144
56
  async function fetchPage(platform, project, environment, applicationId, pageId) {
145
57
  const response = await platform.tenant('GET', project, `/pages/${applicationId}/${pageId}`, { environment });
146
58
  return response?.data ?? {};
147
59
  }
148
60
  /**
149
- * F-8 — platformun `set_event` micro-op'u olay KALDIRAMAZ: `eventValue: null` verildiğinde
150
- * PageSchemaMicroOpApplier kaydı `{ name: 'click' }` olarak (değersiz) bırakır, diziden çıkarmaz;
151
- * ölü bağ şemada artık olarak kalır. Kaldırma yolu tek: şemayı okuyup diziden çıkarmak ve sayfayı
152
- * tam gövdeyle geri yazmak (page_update'in kullandığı PUT — pageUpdateBody her alanı korur).
153
- * `target: 'page'` sayfa düzeyi olayları hedefler.
154
- */
155
- async function applyEventRemovals(platform, project, environment, applicationId, pageId, page, ops) {
156
- const schema = JSON.parse(JSON.stringify(page.schema ?? []));
157
- const pageEvents = Array.isArray(page.events) ? [...page.events] : [];
158
- const removed = [];
159
- let pageEventsTouched = false;
160
- for (const op of ops) {
161
- const target = String(op.target ?? '');
162
- const event = String(op.event ?? '');
163
- if (!target || !event)
164
- throw new PlatformApiError("remove_event için 'target' ve 'event' zorunludur.", 400);
165
- if (target === 'page') {
166
- const before = pageEvents.length;
167
- const kept = pageEvents.filter(entry => String(entry?.name ?? '') !== event);
168
- if (kept.length !== before) {
169
- pageEvents.length = 0;
170
- pageEvents.push(...kept);
171
- pageEventsTouched = true;
172
- removed.push(`page.${event}`);
173
- }
174
- continue;
175
- }
176
- const node = findNode(schema, target);
177
- if (!node)
178
- throw new PlatformApiError(`remove_event hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
179
- const events = Array.isArray(node.events) ? node.events : [];
180
- const kept = events.filter(entry => String(entry?.name ?? '') !== event);
181
- if (kept.length !== events.length)
182
- removed.push(`${target}.${event}`);
183
- node.events = kept;
184
- }
185
- if (removed.length === 0)
186
- return [];
187
- const body = pageUpdateBody(page, pageEventsTouched ? { schema, events: pageEvents } : { schema });
188
- await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
189
- return removed;
190
- }
191
- function findParentNode(nodes, target) {
192
- for (const node of nodes) {
193
- if ((node.children ?? []).some(child => child.elementId === target))
194
- return node;
195
- const deeper = findParentNode(node.children ?? [], target);
196
- if (deeper)
197
- return deeper;
198
- }
199
- return undefined;
200
- }
201
- /**
202
- * `resize_component` gateway emülasyonudur: platformun micro-op sözlüğünde boyut YOKTUR
203
- * (`move_component` yalnız `position` uygular, gövdeye konan `size` 200 döner ve sessizce düşer —
204
- * ölçüldü 09-04). Bu yüzden kurulmuş bir sayfada bir kartın yüksekliğini değiştirmenin tek yolu
205
- * düğümü `remove_component` + `add_component` ile yeniden kurmaktı; o da ona yazan akış
206
- * setter'larını sessizce öldürüyordu (FLOW-018). Op şemayı okur, hedefin boyutunu değiştirir ve
207
- * sayfayı tam gövdeyle geri yazar — `remove_event` ile aynı desen.
208
- *
209
- * Izgarada boyut `blocks[].size`, flex'te `appearance` alanlarındadır; ikisi de kabul edilir ve
210
- * ebeveynin düzeni okunanı etkisiz kılıyorsa uyarı üretilir (PAGE-015 ile aynı yaklaşım: reddetme,
211
- * ama sessiz de kalma).
212
- */
213
- /**
214
- * G-25 — `states` bir prop değil düğüm alanı: `replace_prop` yazamıyor, `move_component` yalnız
215
- * konum uyguluyor ve `resize_component` (G-14) boyutla sınırlıydı. Kurulmuş bir düğümün
216
- * `isWrapper` bayrağını değiştirmenin tek yolu düğümü silip yeniden kurmaktı; 09-05 koşusunda
217
- * bunun bedeli 20 setter'ın sessizce kopmasıydı (G-21). Anahtar kümesi beyaz listeyle sınırlı:
218
- * `states` açık uçlu bir sözlük ve uydurma bayrak sessizce kaydedilirdi.
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.
219
65
  */
220
- const WRITABLE_STATES = new Set(['isWrapper', 'isScrollable', 'isSticky', 'isDroppable', 'isFloating', 'isQuickStack']);
221
- async function applyResizes(platform, project, environment, applicationId, pageId, page, ops) {
222
- const schema = JSON.parse(JSON.stringify(page.schema ?? []));
223
- const applied = [];
224
- const warnings = [];
225
- for (const op of ops) {
226
- const target = String(op.target ?? '');
227
- const size = op.size && typeof op.size === 'object' ? op.size : undefined;
228
- const appearance = op.appearance && typeof op.appearance === 'object' ? op.appearance : undefined;
229
- const states = op.states && typeof op.states === 'object' ? op.states : undefined;
230
- const breakpoint = Number(op.breakpoint ?? 1024);
231
- if (!target)
232
- throw new PlatformApiError("resize_component için 'target' zorunludur.", 400);
233
- if (!size && !appearance && !states) {
234
- 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);
235
- }
236
- if (states) {
237
- const unknownStates = Object.keys(states).filter(key => !WRITABLE_STATES.has(key));
238
- if (unknownStates.length > 0) {
239
- throw new PlatformApiError(`resize_component '${target}': bilinmeyen states anahtarı (${unknownStates.join(', ')}). Yazılabilir bayraklar: ${[...WRITABLE_STATES].join(', ')}.`, 400);
240
- }
241
- }
242
- const node = findNode(schema, target);
243
- if (!node)
244
- throw new PlatformApiError(`resize_component hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
245
- const parent = findParentNode(schema, target);
246
- const inFlex = parent !== undefined && isFlexContainer(parent);
247
- if (size) {
248
- const blocks = Array.isArray(node.blocks) ? node.blocks : [];
249
- const block = blocks.find(entry => Number(entry?.breakpoint) === breakpoint);
250
- if (!block) {
251
- 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);
252
- }
253
- const current = (block.size ?? {});
254
- block.size = { w: Number(size.w ?? current.w ?? 0), h: Number(size.h ?? current.h ?? 0) };
255
- applied.push(`${target}.size=${JSON.stringify(block.size)}`);
256
- if (inFlex) {
257
- 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.`);
258
- }
259
- }
260
- if (states) {
261
- node.states = { ...(node.states ?? {}), ...states };
262
- applied.push(`${target}.states=${JSON.stringify(states)}`);
263
- if (states.isWrapper === true) {
264
- 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.`);
265
- }
266
- }
267
- if (appearance) {
268
- node.appearance = { ...(node.appearance ?? {}), ...appearance };
269
- applied.push(`${target}.appearance=${JSON.stringify(appearance)}`);
270
- if (!inFlex && parent !== undefined) {
271
- 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.`);
272
- }
273
- }
274
- }
275
- if (applied.length === 0)
276
- return { applied, warnings };
277
- const body = pageUpdateBody(page, { schema });
278
- await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
279
- return { applied, warnings };
280
- }
281
- async function applyPropTypes(platform, project, environment, applicationId, pageId, page, ops) {
282
- const schema = JSON.parse(JSON.stringify(page.schema ?? []));
283
- const { applied, missingTargets } = applyPropTypesToSchema(schema, ops);
284
- if (missingTargets.length > 0) {
285
- throw new PlatformApiError(`set_prop_type hedefi bulunamadı: '${missingTargets[0]}'. page_schema_get ile elementId'leri listeleyin.`, 400);
286
- }
287
- if (applied.length === 0)
288
- return [];
289
- const body = pageUpdateBody(page, { schema });
290
- await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
291
- return applied;
292
- }
293
- 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) {
294
71
  const config = platform.config;
295
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.");
296
73
  /** Sayfa ve uç araçları aynı kimlik sözleşmesini paylaşır (G-20); çözüm tokenVault'ta yaşar. */
@@ -401,12 +178,23 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
401
178
  appFunctions: runtimeFunctions(body.appFunctions),
402
179
  pageFunctions: runtimeFunctions(body.pageFunctions),
403
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
+ : {}),
404
191
  note: resolveStyles
405
- ? "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."
406
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."
407
194
  };
408
195
  }));
409
196
  const screenshotter = injectedScreenshotter ?? new StudioScreenshotter(config);
197
+ const captureMemory = injectedMemory ?? sharedCaptureMemory;
410
198
  const maxInlinePngBytes = 5 * 1024 * 1024;
411
199
  server.registerTool('page_screenshot', {
412
200
  title: 'Sayfanın ekran görüntüsünü al',
@@ -429,21 +217,42 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
429
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."),
430
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."),
431
219
  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"),
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"),
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>'"),
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"),
435
224
  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)."),
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)."),
437
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ı."),
438
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."),
439
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')
440
239
  },
441
240
  annotations: readOnly
442
- }, 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 }) => {
443
242
  try {
444
243
  if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
445
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.");
446
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
+ }
252
+ const incompleteDrag = actions?.findIndex(step => step.action === 'drag' && (step.target === undefined || step.to === undefined)) ?? -1;
253
+ if (incompleteDrag >= 0) {
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'}.`);
255
+ }
447
256
  if (!config.studioBaseUrl) {
448
257
  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
258
  }
@@ -476,11 +285,12 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
476
285
  const size = viewport ?? { width: 1280, height: 800 };
477
286
  const extraCookies = Object.entries(cookies ?? {}).map(([name, value]) => ({ name, value }));
478
287
  const identity = anonymous === true ? 'anonymous' : asUser === undefined ? 'gateway' : 'asUser';
288
+ const identityToken = anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken();
479
289
  const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
480
290
  const shot = await screenshotter.capture({
481
291
  url,
482
292
  apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
483
- token: anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken(),
293
+ token: identityToken,
484
294
  ...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
485
295
  cookieUrl: config.studioBaseUrl,
486
296
  viewport: size,
@@ -489,8 +299,78 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
489
299
  ...(captureUnready === true ? { captureUnready: true } : {}),
490
300
  ...(layoutReport === true ? { layoutReport: true } : {}),
491
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 }),
492
310
  ...(actions === undefined ? {} : { actions: actions })
493
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.`);
494
374
  const meta = {
495
375
  url,
496
376
  environment: resolvedEnvironment,
@@ -501,8 +381,41 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
501
381
  fullPage: fullPage === true,
502
382
  identity,
503
383
  cookieNames,
504
- 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
+ : {}),
505
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
+ }),
506
419
  ...(shot.contentHeight === undefined ? {} : { contentHeight: shot.contentHeight }),
507
420
  ...(shot.fullPageTruncated
508
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.` }
@@ -555,13 +468,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
555
468
  runtimeErrors: shot.runtimeErrors,
556
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.`
557
470
  }),
558
- ...(shot.actions?.some(outcome => !outcome.found)
559
- ? { 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
- : {}),
561
- // 'wait' bir eylem değil bekleyiştir: akış sayacı ona bağlanmaz, notu tetiklemez (33/S-1).
562
- ...(shot.actions?.some(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0)
563
- ? { 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.` }
564
- : {}),
471
+ ...(actionNotes.length === 0 ? {} : { actionsNote: actionNotes.join('\n') }),
565
472
  ...(shot.layout === undefined
566
473
  ? {}
567
474
  : shot.layout.length === 0
@@ -576,6 +483,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
576
483
  try {
577
484
  // B-6: klasör yoksa ENOENT ile düşüyordu; koşu ajanı her seferinde klasörü elle açıyordu.
578
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.');
579
488
  await writeFile(absolute, shot.png);
580
489
  }
581
490
  catch (error) {
@@ -584,12 +493,19 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
584
493
  }
585
494
  return toolResult({ path: absolute, ...meta }, config);
586
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
+ }
587
503
  if (shot.png.byteLength > maxInlinePngBytes) {
588
- 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.`);
589
505
  }
590
506
  return {
591
507
  content: [
592
- { type: 'image', data: shot.png.toString('base64'), mimeType: 'image/png' },
508
+ { type: 'image', data: shot.png.toString('base64'), mimeType: shot.mimeType ?? 'image/png' },
593
509
  { type: 'text', text: JSON.stringify(meta) }
594
510
  ],
595
511
  structuredContent: meta
@@ -601,7 +517,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
601
517
  });
602
518
  server.registerTool('page_schema_get', {
603
519
  title: 'Sayfa şemasını oku',
604
- 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.",
605
521
  inputSchema: {
606
522
  project: projectParam,
607
523
  environment: environmentParam,
@@ -614,17 +530,18 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
614
530
  }, handle(config, async ({ project, environment, applicationId, pageId, mode, componentId }) => {
615
531
  const page = await fetchPage(platform, project, environment, applicationId, pageId);
616
532
  const updatedAt = page.audit?.updatedAt ?? page.audit?.createdAt;
533
+ const version = page.version;
617
534
  if (mode === 'full')
618
- return { updatedAt, schema: page.schema ?? [] };
535
+ return { version, updatedAt, schema: page.schema ?? [] };
619
536
  if (mode === 'subtree') {
620
537
  if (!componentId)
621
538
  throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
622
539
  const subtree = findNode(page.schema, componentId);
623
540
  if (!subtree)
624
541
  throw new PlatformApiError(`Sayfada '${componentId}' id'li bir komponent yok; page_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
625
- return { updatedAt, schema: subtree };
542
+ return { version, updatedAt, schema: subtree };
626
543
  }
627
- return { updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
544
+ return { version, updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
628
545
  }));
629
546
  server.registerTool('page_state', {
630
547
  title: 'Sayfa durumunu getir',
@@ -642,26 +559,33 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
642
559
  const propTypeIssues = setPropTypeErrors(allOps.filter(entry => entry.op === 'set_prop_type'));
643
560
  if (propTypeIssues.length > 0)
644
561
  throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
645
- // Gateway emülasyonu olan op'lar platformun doğrulama ucunu tanımadığı için gövdeden ayrılır;
646
- // kuru koşu ile yazma AYNI kararı vermeli (P0-2 Step 3).
647
- 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');
648
565
  const normalized = normalizeSchemaOps(opRecords);
649
- const validationIssues = schemaOpValidationErrors(opRecords);
566
+ const validationIssues = [
567
+ ...schemaOpValidationErrors(opRecords).map(issue => `PAGE-016 — ${issue}`),
568
+ ...codePropTemplateErrors(opRecords).map(issue => `PAGE-032 — ${issue}`)
569
+ ];
570
+ const ordering = await orderByIndex(opRecords, () => fetchPage(platform, project, environment, applicationId, pageId));
650
571
  const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
651
572
  environment,
652
- body: { ops: opRecords }
573
+ body: { ops: [...allOps.filter(entry => entry.op === 'resize_component').flatMap(resizeServerOps), ...ordering.ops] }
653
574
  })) ?? {});
654
575
  if (validationIssues.length > 0)
655
- response.validationWarnings = validationIssues.map(issue => `PAGE-016 — ${issue}`);
576
+ response.validationWarnings = validationIssues;
577
+ if (ordering.warnings.length > 0)
578
+ response.layoutWarnings = ordering.warnings;
656
579
  return withNormalization(response, [
657
580
  ...componentDefaultsNote(normalized.components),
658
581
  ...validationNote(normalized.validations),
659
- ...replacePropNote(normalized.props)
582
+ ...replacePropNote(normalized.props),
583
+ ...flexIndexOrderNote(ordering.moved)
660
584
  ]);
661
585
  }));
662
586
  server.registerTool('page_schema_patch', {
663
587
  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.",
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.",
665
589
  inputSchema: {
666
590
  project: projectParam,
667
591
  environment: environmentParam,
@@ -669,28 +593,31 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
669
593
  pageId: guid,
670
594
  ops: opsParam.optional(),
671
595
  operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"),
672
- 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"),
673
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."),
674
599
  view: writeViewParam
675
600
  },
676
601
  annotations: updates
677
- }, 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 }) => {
678
603
  const allOps = microOps('page_schema_patch', ops, operations).map(op => op);
679
604
  const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
680
605
  const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
681
606
  const propTypeOps = allOps.filter(entry => entry.op === 'set_prop_type');
682
607
  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);
608
+ const rejected = [];
609
+ const reject = (code, entry, messages) => {
610
+ for (const message of messages) {
611
+ rejected.push({ index: allOps.indexOf(entry), op: String(entry.op ?? ''), target: typeof entry.target === 'string' ? entry.target : null, code, message });
612
+ }
613
+ };
614
+ for (const entry of propTypeOps)
615
+ reject('SET-PROP-TYPE', entry, setPropTypeErrors([entry]));
616
+ for (const entry of opRecords.filter(record => record.op === 'move_component' && record.size !== undefined)) {
617
+ 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
618
  }
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);
619
+ for (const entry of opRecords.filter(record => (record.op === 'remove_component' || record.op === 'move_component') && record.target === PAGE_CONTAINER_ELEMENT_ID)) {
620
+ 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
621
  }
695
622
  let cachedPage;
696
623
  const readPage = async () => {
@@ -705,19 +632,10 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
705
632
  }
706
633
  }
707
634
  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
- }
635
+ for (const entry of opRecords)
636
+ reject('PAGE-016', entry, schemaOpValidationErrors([entry]));
637
+ for (const entry of opRecords)
638
+ reject('PAGE-032', entry, codePropTemplateErrors([entry]));
721
639
  if (opRecords.some(entry => entry.op === 'set_event')) {
722
640
  const typesByElementId = new Map();
723
641
  try {
@@ -728,14 +646,27 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
728
646
  }
729
647
  collectTypes(opRecords.map(entry => entry.component).filter(Boolean), typesByElementId);
730
648
  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);
649
+ for (const entry of opRecords)
650
+ reject('PAGE-008', entry, unknownEventOps([entry], typesByElementId, eventsBySlug));
651
+ }
652
+ if (rejected.length > 0)
653
+ throw new PlatformApiError(rejectedOpsMessage(rejected), 400);
654
+ const wantsRootAdd = opRecords.some(entry => entry.op === 'add_component' && (entry.parent === undefined || entry.parent === null || entry.parent === 'root'));
655
+ let retargeted = [];
656
+ if (wantsRootAdd) {
657
+ const page = await readPage();
658
+ if ((page.schema ?? []).some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID)) {
659
+ retargeted = retargetRootAdds(opRecords, PAGE_CONTAINER_ELEMENT_ID);
660
+ }
734
661
  }
662
+ const ordering = await orderByIndex(opRecords, readPage);
663
+ const sentOps = ordering.ops;
735
664
  // Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
736
665
  // silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
737
666
  let deletedFlows = [];
738
- 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)))) {
739
670
  try {
740
671
  const page = await readPage();
741
672
  deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
@@ -755,6 +686,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
755
686
  const formWarnings = [...formBindingWarnings(opRecords), ...formSubmitButtonWarnings(opRecords)];
756
687
  let flowWarnings = [];
757
688
  let layoutWarnings = [];
689
+ let flowsBeforePatch;
758
690
  if (opRecords.some(entry => entry.op === 'remove_component')) {
759
691
  try {
760
692
  const page = await readPage();
@@ -768,7 +700,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
768
700
  try {
769
701
  const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
770
702
  const page = await readPage();
771
- flowWarnings = [...flowWarnings, ...staleSetterWarnings(hosted?.data ?? [], opRecords, page.schema)];
703
+ flowsBeforePatch = hosted?.data ?? [];
704
+ flowWarnings = [...flowWarnings, ...staleSetterWarnings(flowsBeforePatch, opRecords, page.schema)];
772
705
  }
773
706
  catch {
774
707
  // akış listesi okunamadı
@@ -781,7 +714,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
781
714
  try {
782
715
  const page = await readPage();
783
716
  const simulated = JSON.parse(JSON.stringify(page.schema ?? []));
784
- const touched = simulateLayoutOps(simulated, opRecords);
717
+ const touched = simulateLayoutOps(simulated, sentOps);
785
718
  // PAGE-013 op'lar UYGULANDIKTAN SONRAKİ ağaca bakar: tek başına eklenen düğüm masum
786
719
  // görünür, iç içeliği ancak hedef parent zinciri belli olunca anlarsın.
787
720
  nestedAfterOps = nestedRepeaterNodes(simulated);
@@ -790,7 +723,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
790
723
  ...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
791
724
  ...stringStyleWarning(stringStyleNodes(simulated)),
792
725
  ...inertFlexPropWarning(inertFlexPropNodes(simulated)),
793
- ...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')
726
+ ...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'),
727
+ ...spacingLayerWarnings(simulated, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')
794
728
  ];
795
729
  }
796
730
  catch {
@@ -799,30 +733,25 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
799
733
  }
800
734
  if (nestedAfterOps.length > 0)
801
735
  throw new PlatformApiError(nestedRepeaterMessage(nestedAfterOps), 400);
802
- const removedEvents = removeEventOps.length > 0
803
- ? await applyEventRemovals(platform, project, environment, applicationId, pageId, await readPage(), removeEventOps)
804
- : [];
805
- if (removeEventOps.length > 0)
806
- cachedPage = undefined;
807
- const resized = resizeOps.length > 0
808
- ? await applyResizes(platform, project, environment, applicationId, pageId, await readPage(), resizeOps)
809
- : { applied: [], warnings: [] };
810
- if (resizeOps.length > 0)
811
- cachedPage = undefined;
812
- const propTypes = propTypeOps.length > 0
813
- ? await applyPropTypes(platform, project, environment, applicationId, pageId, await readPage(), propTypeOps)
814
- : [];
815
- if (propTypeOps.length > 0)
816
- cachedPage = undefined;
817
- const response = opRecords.length > 0
818
- ? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: opRecords } })) ?? {})
819
- : { valid: true };
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];
740
+ layoutWarnings = [...ordering.warnings, ...layoutWarnings];
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
+ })) ?? {});
820
749
  if (view !== 'full')
821
750
  outlineWriteResponse(response);
822
751
  if (removedEvents.length > 0)
823
752
  response.removedEvents = removedEvents;
824
- if (propTypes.length > 0)
825
- response.propTypes = propTypes;
753
+ if (propTypes.applied.length > 0)
754
+ response.propTypes = propTypes.applied;
826
755
  if (resized.applied.length > 0)
827
756
  response.resized = resized.applied;
828
757
  if (resized.warnings.length > 0)
@@ -833,12 +762,25 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
833
762
  response.formWarnings = formWarnings;
834
763
  if (flowWarnings.length > 0)
835
764
  response.flowWarnings = flowWarnings;
765
+ // FLOW-018 yazma sonrası (G-04): uyarı yamadan önce "kopabilir" der; kopup kopmadığını ancak
766
+ // akışları yeniden okumak söyler. Okuma başarısız olursa yama yine geçerlidir.
767
+ if (flowsBeforePatch && flowWarnings.length > 0 && opRecords.some(entry => entry.op === 'add_component')) {
768
+ try {
769
+ const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
770
+ const lost = lostSetterTargets(flowsBeforePatch, hosted?.data ?? [], opRecords);
771
+ if (lost.length > 0)
772
+ response.flowTargetsLost = lost;
773
+ }
774
+ catch {
775
+ // akış listesi yeniden okunamadı
776
+ }
777
+ }
836
778
  if (layoutWarnings.length > 0)
837
779
  response.layoutWarnings = layoutWarnings;
838
780
  // G-23 micro-op ayağı: yama 200 döner ve `appliedPatch` op'un ŞEMAYA yazıldığını söyler, ama
839
781
  // alanın gerçekten KAYDEDİLDİĞİNİ söylemez. Yamadan sonra şemayı bir kez okuyup op'un yazmak
840
782
  // 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');
783
+ const contentOps = sentOps.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
842
784
  let opDrops = [];
843
785
  if (contentOps.length > 0) {
844
786
  try {
@@ -854,7 +796,8 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
854
796
  ...componentDefaultsNote(normalized.components),
855
797
  ...validationNote(normalized.validations),
856
798
  ...catalogNotes,
857
- ...replacePropNote(normalized.props)
799
+ ...replacePropNote(normalized.props),
800
+ ...flexIndexOrderNote(ordering.moved)
858
801
  ]), opDrops);
859
802
  }));
860
803
  server.registerTool('page_create', {
@@ -914,7 +857,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
914
857
  saved.schema = saved.schema.map(node => summarize(node));
915
858
  }
916
859
  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ı)')];
860
+ 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
861
  if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
919
862
  return normalized;
920
863
  return { ...normalized, layoutWarnings: createWarnings };
@@ -948,7 +891,7 @@ export function registerPageTools(server, platform, injectedScreenshotter) {
948
891
  const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
949
892
  const gaps = overrides.schema === undefined
950
893
  ? []
951
- : [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema)), ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
894
+ : [...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
895
  const body = pageUpdateBody(current, overrides);
953
896
  await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
954
897
  const summary = {