@appaflytech/zeyrek-ai-mcp 0.0.5 → 0.0.7

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 (145) hide show
  1. package/.env.example +2 -2
  2. package/.env.production.example +9 -1
  3. package/README.md +14 -38
  4. package/dist/config.js +36 -3
  5. package/dist/knowledge/backendConstraints.js +982 -0
  6. package/dist/knowledge/componentCatalog.js +549 -92
  7. package/dist/knowledge/componentEvents.js +120 -25
  8. package/dist/knowledge/coverage.js +43 -0
  9. package/dist/knowledge/flowGuards.js +30 -0
  10. package/dist/knowledge/functionGuards.js +134 -0
  11. package/dist/knowledge/guideSections.js +49 -0
  12. package/dist/knowledge/hydrate.js +97 -0
  13. package/dist/knowledge/languageCodes.js +36 -0
  14. package/dist/knowledge/languages.js +105 -0
  15. package/dist/knowledge/layoutSummary.js +10 -0
  16. package/dist/knowledge/localization.js +51 -0
  17. package/dist/knowledge/normalize.js +500 -16
  18. package/dist/knowledge/outlines.js +167 -12
  19. package/dist/knowledge/policy.js +14 -5
  20. package/dist/knowledge/propTypes.js +80 -0
  21. package/dist/knowledge/rules.js +961 -44
  22. package/dist/knowledge/schemaGuard.js +199 -0
  23. package/dist/knowledge/styleResolve.js +63 -4
  24. package/dist/observability/trace.js +27 -3
  25. package/dist/platform/csv.js +123 -0
  26. package/dist/platform/errors.js +107 -6
  27. package/dist/platform/http.js +34 -4
  28. package/dist/platform/patch.js +27 -0
  29. package/dist/platform/refs.js +104 -0
  30. package/dist/platform/responses.js +25 -6
  31. package/dist/platform/screenshot.js +544 -16
  32. package/dist/platform/secrets.js +40 -0
  33. package/dist/platform/tokenVault.js +89 -0
  34. package/dist/platform/writeDiff.js +281 -0
  35. package/dist/resources/examples/flow-call-function.json +66 -0
  36. package/dist/resources/examples/function-total.json +70 -0
  37. package/dist/resources/examples/layout-app-shell.json +1 -1
  38. package/dist/resources/examples/microfunction-node-sum.json +56 -0
  39. package/dist/resources/examples/microservice-table-list.json +2 -4
  40. package/dist/resources/examples/page-dynamic-form.json +3 -3
  41. package/dist/resources/examples/page-flex-sections.json +99 -0
  42. package/dist/resources/examples/page-sign-in.json +4 -3
  43. package/dist/resources/guides/component.md +81 -8
  44. package/dist/resources/guides/entity.md +712 -20
  45. package/dist/resources/guides/expressions.md +47 -2
  46. package/dist/resources/guides/external-component.md +9 -0
  47. package/dist/resources/guides/functions.md +262 -0
  48. package/dist/resources/guides/getting-started.md +82 -7
  49. package/dist/resources/guides/layout.md +53 -10
  50. package/dist/resources/guides/merge.md +231 -0
  51. package/dist/resources/guides/microfunction.md +166 -0
  52. package/dist/resources/guides/microservice.md +287 -15
  53. package/dist/resources/guides/middleware.md +89 -1
  54. package/dist/resources/guides/modify-existing.md +23 -0
  55. package/dist/resources/guides/page.md +402 -49
  56. package/dist/resources/guides/translation.md +150 -8
  57. package/dist/resources/guides/view.md +16 -1
  58. package/dist/resources/guides/workflow.md +199 -28
  59. package/dist/resources/knowledge.js +5 -0
  60. package/dist/resources/rules.js +27 -0
  61. package/dist/resources/schema-fingerprint.json +8 -0
  62. package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
  63. package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
  64. package/dist/resources/schemas/CreateBulkRowCommand.json +36 -0
  65. package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
  66. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
  67. package/dist/resources/schemas/CreateEntityCommand.json +52 -12
  68. package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
  69. package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
  70. package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
  71. package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
  72. package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
  73. package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
  74. package/dist/resources/schemas/CreateMicroserviceCommand.json +145 -3
  75. package/dist/resources/schemas/CreateQueryCommand.json +56 -3
  76. package/dist/resources/schemas/CreateRowCommand.json +38 -0
  77. package/dist/resources/schemas/CreateViewCommand.json +13 -10
  78. package/dist/resources/schemas/CreateWorkflowCommand.json +106 -14
  79. package/dist/resources/schemas/DataJobDto.json +267 -0
  80. package/dist/resources/schemas/ExportRowsCommand.json +172 -0
  81. package/dist/resources/schemas/Filter.json +3 -1
  82. package/dist/resources/schemas/GetRowsQuery.json +6 -1
  83. package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
  84. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
  85. package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
  86. package/dist/resources/schemas/ImportPlanDto.json +260 -0
  87. package/dist/resources/schemas/ImportSampleDto.json +90 -0
  88. package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
  89. package/dist/resources/schemas/PreviewMicroserviceCommand.json +135 -1
  90. package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
  91. package/dist/resources/schemas/PreviewWorkflowCommand.json +45 -9
  92. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
  93. package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
  94. package/dist/resources/schemas/RunImportCommand.json +62 -0
  95. package/dist/resources/schemas/StepDto.json +34 -3
  96. package/dist/resources/schemas/UpdateBulkRowCommand.json +36 -0
  97. package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
  98. package/dist/resources/schemas/UpdateEntityCommand.json +52 -11
  99. package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
  100. package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
  101. package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
  102. package/dist/resources/schemas/UpdateMicroserviceCommand.json +147 -4
  103. package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
  104. package/dist/resources/schemas/UpdateRowCommand.json +40 -0
  105. package/dist/resources/schemas/UpdateViewCommand.json +15 -10
  106. package/dist/resources/schemas/UpdateWorkflowCommand.json +106 -14
  107. package/dist/server.js +17 -1
  108. package/dist/tools/access.js +3 -3
  109. package/dist/tools/activities.js +40 -0
  110. package/dist/tools/applications.js +1 -1
  111. package/dist/tools/automations.js +76 -34
  112. package/dist/tools/common.js +55 -0
  113. package/dist/tools/components.js +16 -7
  114. package/dist/tools/datasources.js +83 -21
  115. package/dist/tools/dependencies.js +1 -1
  116. package/dist/tools/deployments.js +34 -2
  117. package/dist/tools/dtos.js +25 -12
  118. package/dist/tools/entities.js +113 -28
  119. package/dist/tools/environments.js +52 -0
  120. package/dist/tools/externalComponents.js +9 -3
  121. package/dist/tools/files.js +57 -42
  122. package/dist/tools/flows.js +205 -13
  123. package/dist/tools/folders.js +7 -2
  124. package/dist/tools/functions.js +331 -0
  125. package/dist/tools/globalFunctions.js +63 -0
  126. package/dist/tools/importSchemas.js +79 -0
  127. package/dist/tools/imports.js +241 -0
  128. package/dist/tools/layouts.js +103 -29
  129. package/dist/tools/merges.js +113 -0
  130. package/dist/tools/microfunctions.js +155 -0
  131. package/dist/tools/microservices.js +270 -40
  132. package/dist/tools/organizations.js +5 -6
  133. package/dist/tools/pages.js +421 -104
  134. package/dist/tools/platformInfo.js +155 -11
  135. package/dist/tools/projects.js +183 -33
  136. package/dist/tools/queries.js +32 -10
  137. package/dist/tools/revisions.js +94 -0
  138. package/dist/tools/rows.js +484 -50
  139. package/dist/tools/screenWrite.js +68 -0
  140. package/dist/tools/screens.js +28 -0
  141. package/dist/tools/templates.js +32 -5
  142. package/dist/tools/translations.js +24 -8
  143. package/dist/tools/views.js +20 -10
  144. package/dist/tools/workflows.js +56 -21
  145. package/package.json +7 -2
@@ -1,15 +1,57 @@
1
- import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
2
- import { orphanedFlowWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, blockSizeNote, heightTypeNote, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, invalidScreenEventNames, invalidWidgetTypeNodes, withNormalization, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
3
- import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode } from './screens.js';
4
- import { applyCatalogLayoutRules, fetchComponentEventNames } from './components.js';
5
- import { formBindingWarnings } from '../knowledge/coverage.js';
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';
3
+ import { functionSignature } from '../knowledge/functionGuards.js';
4
+ import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, outlineAppliedPatch, findNode, flowPath } from './screens.js';
5
+ import { fetchComponentEventNames } from './components.js';
6
+ import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
7
+ import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
8
+ import { schemaOpDrops, silentlyDropped, unwrapProps, withWriteDiff } from '../platform/writeDiff.js';
6
9
  import { PlatformApiError } from '../platform/errors.js';
7
10
  import { handle, toolResult, toolError } from '../platform/responses.js';
8
11
  import { StudioScreenshotter } from '../platform/screenshot.js';
9
- import { writeFile } from 'node:fs/promises';
10
- import { resolve } from 'node:path';
12
+ import { summarizeLayout } from '../knowledge/layoutSummary.js';
13
+ import { normalizeLanguageCode } from '../knowledge/languageCodes.js';
14
+ import { readProjectLanguages } from '../knowledge/languages.js';
15
+ import { resolveIdentityToken } from '../platform/tokenVault.js';
16
+ import { mkdir, writeFile } from 'node:fs/promises';
17
+ import { dirname, resolve } from 'node:path';
18
+ import { applyPropTypesToSchema, setPropTypeErrors } from '../knowledge/propTypes.js';
11
19
  import { z } from 'zod';
12
- 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). target = elementId.");
20
+ /**
21
+ * Runtime paketindeki `appFunctions`/`pageFunctions` (UiFunctionRuntimeDto: id, name, parameters,
22
+ * body, isAsync) — imza taşınır, GÖVDE TAŞINMAZ (MAX_RESPONSE_BYTES). Ajan Functions.<ad>(…)
23
+ * yazarken adı ve parametre sırasını buradan alır (G-06).
24
+ */
25
+ function runtimeFunctions(value) {
26
+ return (Array.isArray(value) ? value : [])
27
+ .filter((entry) => Boolean(entry) && typeof entry === 'object')
28
+ .map(entry => ({
29
+ id: entry.id,
30
+ name: entry.name,
31
+ signature: functionSignature(entry),
32
+ isAsync: entry.isAsync === true,
33
+ parameters: (Array.isArray(entry.parameters) ? entry.parameters : [])
34
+ .filter((parameter) => Boolean(parameter) && typeof parameter === 'object')
35
+ .map(parameter => ({ name: parameter.name, required: parameter.required === true, defaultValue: parameter.defaultValue ?? null }))
36
+ }));
37
+ }
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} | 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.");
13
55
  /**
14
56
  * PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
15
57
  * komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
@@ -37,17 +79,6 @@ function collectTypes(nodes, into) {
37
79
  collectTypes(node.children, into);
38
80
  }
39
81
  }
40
- function invalidComponentNames(nodes) {
41
- const found = [];
42
- for (const node of nodes ?? []) {
43
- const name = node.name;
44
- if (typeof name === 'string' && name.length > 0 && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name.replace(/-/g, ''))) {
45
- found.push(`'${name}' (${node.elementId ?? '?'})`);
46
- }
47
- found.push(...invalidComponentNames(node.children));
48
- }
49
- return found;
50
- }
51
82
  function removeNodeFromTree(nodes, target) {
52
83
  for (let index = 0; index < nodes.length; index++) {
53
84
  if (nodes[index].elementId === target)
@@ -157,8 +188,113 @@ async function applyEventRemovals(platform, project, environment, applicationId,
157
188
  await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
158
189
  return removed;
159
190
  }
160
- export function registerPageTools(server, platform) {
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.
219
+ */
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) {
161
294
  const config = platform.config;
295
+ 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
+ /** Sayfa ve uç araçları aynı kimlik sözleşmesini paylaşır (G-20); çözüm tokenVault'ta yaşar. */
297
+ const resolveIdentity = resolveIdentityToken;
162
298
  server.registerTool('page_list', {
163
299
  title: 'Sayfaları listele',
164
300
  description: 'Uygulamanın sayfalarını (id, name, slug) listeler; hedef sayfayı bulmak için ilk adım.',
@@ -192,6 +328,7 @@ export function registerPageTools(server, platform) {
192
328
  name: page.name,
193
329
  slug: page.slug,
194
330
  browserTitle: page.browserTitle ?? '',
331
+ browserTitleKey: page.browserTitleKey ?? '',
195
332
  isHomepage: page.isHomepage === true,
196
333
  layoutId: page.layoutId ?? layout.id ?? null,
197
334
  layoutName: layout.name,
@@ -208,7 +345,7 @@ export function registerPageTools(server, platform) {
208
345
  }));
209
346
  server.registerTool('page_runtime_get', {
210
347
  title: 'Sayfanın runtime çıktısını getir',
211
- description: "Çıkan uygulamanın sayfa için aldığı GERÇEK paketi döner (GET /{project}/{application}/p/{slug} — sayfanın ham serve yolu budur; Studio preview ve runtime aynı ucu tüketir): sayfa şeması + layout views'ü + sayfa olayları + flow'lar + değişken anahtarları. Kurduğun sayfanın SON hâlini doğrulamanın yolu budur (layout-slot yerleşimi dahil — Kural 1f). DİKKAT: sayfaya bağlı middleware'ler bu çağrıda GERÇEKTEN koşar; middleware reddederse yanıt middlewareBlocked ile döner — korumalı sayfada bu beklenen davranıştır ve kendisi bir doğrulamadır.",
348
+ description: "Çıkan uygulamanın sayfa için aldığı GERÇEK paketi döner (GET /{project}/{application}/p/{slug} — sayfanın ham serve yolu budur; Studio preview ve runtime aynı ucu tüketir): sayfa şeması + layout views'ü + sayfa olayları + flow'lar + değişken anahtarları (variableKeys: middleware değişkenleri, pageVariableKeys/appVariableKeys: state.page/state.app anahtarları) + kullanıcı fonksiyonu imzaları (appFunctions/pageFunctions — bir ifadeye Functions.<ad>(…) yazmadan önce adı ve parametre sırasını buradan alın; gövde taşınmaz, function_get ile okunur). Kurduğun sayfanın SON hâlini doğrulamanın yolu budur (layout-slot yerleşimi dahil — Kural 1f). DİKKAT: sayfaya bağlı middleware'ler bu çağrıda GERÇEKTEN koşar; middleware reddederse yanıt middlewareBlocked ile döner — korumalı sayfada bu beklenen davranıştır ve kendisi bir doğrulamadır.",
212
349
  inputSchema: {
213
350
  project: projectParam,
214
351
  environment: environmentParam,
@@ -216,13 +353,20 @@ export function registerPageTools(server, platform) {
216
353
  pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
217
354
  view: z.enum(['summary', 'full']).optional().describe('summary (varsayılan): şema/layout ağaçları id/name/type özetine indirgenir. full: ham gövde — büyük olabilir'),
218
355
  resolveStyles: z.boolean().optional().describe("summary görünümünde her düğüme EFEKTİF stilini ekler: tema componentStyles → preset → düğümün kendi stili kaskadı çözülür (aibox'ta tipli stil grupları da derlenir ve TABAN katman olur). styleLayers hangi katmanın konuştuğunu söyler — 'bu kutu neden böyle görünüyor' sorusunun cevabı budur."),
219
- anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı olmadan gider — 'korumalı sayfa tokensız ne döner, şema sızıyor mu?' sorusunun cevabı budur. Korumalı sayfada middlewareBlocked bekleyin.")
356
+ anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı ve çerezi olmadan gider — 'korumalı sayfa tokensız ne döner, şema sızıyor mu?' sorusunun cevabı budur. Korumalı sayfada middlewareBlocked bekleyin."),
357
+ asUser: asUserParam
220
358
  },
221
359
  annotations: readOnly
222
- }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous }) => {
360
+ }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous, asUser }) => {
223
361
  let response;
362
+ if (anonymous === true && asUser !== undefined) {
363
+ throw new PlatformApiError('page_runtime_get: anonymous:true ile asUser birlikte verilemez — biri kimliksiz isteği, diğeri belirli bir üyenin kimliğiyle atılan isteği ister.');
364
+ }
365
+ // page_screenshot çerez, bu tool başlık taşıyordu; çerez okuyan middleware ikincisinden
366
+ // yapısal olarak geçemiyordu. Kimlik artık iki kanalda birden gider (Y-02).
367
+ const identity = asUser === undefined ? {} : { headers: { authorization: `Bearer ${resolveIdentity(asUser)}` } };
224
368
  try {
225
- response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous });
369
+ response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous, mirrorAuthorizationCookie: true, ...identity });
226
370
  }
227
371
  catch (error) {
228
372
  if (error instanceof PlatformApiError && typeof error.status === 'number' && error.status >= 300) {
@@ -245,24 +389,28 @@ export function registerPageTools(server, platform) {
245
389
  const asNodes = (value) => (Array.isArray(value) ? value : []);
246
390
  const named = (value) => (Array.isArray(value) ? value : []).map(entry => ({ id: entry.id, name: entry.name, slug: entry.slug }));
247
391
  return {
248
- page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, isHomepage: body.isHomepage },
392
+ page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, browserTitleKey: body.browserTitleKey, isHomepage: body.isHomepage },
249
393
  events: body.events ?? [],
250
394
  layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(node => summarize(node, styleSource)) } : null,
251
395
  schema: asNodes(body.schema).map(node => summarize(node, styleSource)),
252
396
  widgets: named(body.widgets),
253
397
  flows: named(body.flows),
254
398
  variableKeys: Object.keys((body.variables ?? {})),
399
+ pageVariableKeys: parseVariableEntries(body.pageVariables).map(entry => entry.key),
400
+ appVariableKeys: parseVariableEntries(body.appVariables).map(entry => entry.key),
401
+ appFunctions: runtimeFunctions(body.appFunctions),
402
+ pageFunctions: runtimeFunctions(body.pageFunctions),
255
403
  hasTheme: Boolean(body.project?.theme),
256
404
  note: resolveStyles
257
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."
258
406
  : "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."
259
407
  };
260
408
  }));
261
- const screenshotter = new StudioScreenshotter(config);
409
+ const screenshotter = injectedScreenshotter ?? new StudioScreenshotter(config);
262
410
  const maxInlinePngBytes = 5 * 1024 * 1024;
263
411
  server.registerTool('page_screenshot', {
264
412
  title: 'Sayfanın ekran görüntüsünü al',
265
- description: "Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve hata/boş durum karesi yine döner — bu da bir doğrulamadır. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir.",
413
+ description: "Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve Studio'nun çizdiği hata durumu karesi döner — bu da bir doğrulamadır. YÜKLENMEMİŞ SAYFA ASLA ÇEKİLMEZ: kare, runtime çizilip mount akışları bitince alınır (yanıtta pageState + readyAfterMs); bütçe dolduğunda sayfa hâlâ iskelette ya da akış koşuyorsa çağrı ne görüldüğünü söyleyen bir hatayla döner, boş/yarım kare dönmez. Sayfa akışı bir adımda düşerse (patlayan ifade, hata dönen uç) yanıt runtimeErrors taşır ve hangi komponentin hangi prop'unun YAZILMADIĞINI söyler — kare 'çalışıyor' görünürken sessizce boş kalan komponenti ancak bu yakalar. Kare DİLİ ve TEMASI seçilebilir: 'language' adrese ?lang ekler (çeviriye bağlı prop'lar da çok dilli alanların satır değerleri de o dilde çözülür), 'colorScheme' ziyaretçinin tema tercihini sayfa açılmadan eker — ikisi de sayfaya dil değiştirici ya da theme-toggle koymayı GEREKTİRMEZ, yani referansa sadık bir tasarımın ikinci dili ve koyu teması bozulmadan kanıtlanır. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir. DAL ORTAMI DA GÖRÜNTÜLENİR (2026-09-09'dan beri): 'development' /dev, 'production' /view, dal ise /branch/{dal} şeridinden açılır ve sayfanın BÜTÜN istekleri (şema da akışlar da) o dala gider — dalda kurulan sayfa artık merge'i beklemeden gözle doğrulanır (platform_guide('merge') §Dalda görsel doğrulama).",
266
414
  inputSchema: {
267
415
  project: projectParam,
268
416
  environment: environmentParam,
@@ -273,50 +421,161 @@ export function registerPageTools(server, platform) {
273
421
  height: z.number().int().min(240).max(2160)
274
422
  }).optional().describe('Varsayılan 1280x800 (deviceScaleFactor 1)'),
275
423
  fullPage: z.boolean().optional().describe('true: kaydırılan kısım dahil tüm sayfa; varsayılan yalnız viewport karesi'),
276
- delayMs: z.number().int().min(0).max(10000).optional().describe('Animasyon/geç gelen veri için EK bekleme; 600 ms yerleşme beklemesi her zaman uygulanır'),
277
- anonymous: z.boolean().optional().describe("true: NG_AccessToken çerezi enjekte edilmez — anonim ziyaretçinin gördüğü kare. Korumalı sayfada middlewareBlocked bekleyin."),
424
+ delayMs: z.number().int().min(0).max(60000).optional().describe("Hazırlık bütçesine EK süre (ms). Varsayılan bütçe 15 sn'dir ve kare sayfa hazır olur olmaz alınır (akışsız giriş ekranı ~1 sn, dashboard mount akışları bitince) — normalde vermeye gerek yok. Yalnız sayfa gerçekten uzun sürüyorsa (soğuk Vite derlemesi, ağır mount akışı) büyütün: bütçe dolduğunda sayfa hâlâ hazır değilse çağrı hatayla döner, kare alınmaz. Sayfanın AÇILMASI için beklenen 20 sn'lik navigasyon süresi de bu kadar uzar — soğuk Studio'da tek düğme budur (09-08 S-8)."),
425
+ captureUnready: z.boolean().optional().describe("true: TEŞHİS kipi — bütçe dolduğunda sayfa hazır olmasa da kare alınır ve yanıt pageState: 'not-ready' taşır (iskelet/yarım kare görülebilir). Varsayılan false: yüklenmemiş sayfa çekilmez, hata döner."),
426
+ anonymous: z.boolean().optional().describe("true: hiçbir çerez enjekte edilmez — anonim ziyaretçinin gördüğü kare. Korumalı sayfada middlewareBlocked bekleyin."),
427
+ asUser: asUserParam,
428
+ expectFailure: z.boolean().optional().describe("true: bu çekimde bir akış adımının düşmesi BEKLENİYOR (kasıtlı negatif test — yanlış parola, yetkisiz kullanıcı, geçersiz gövde). Kare yine alınır ve runtimeErrors yine dolar, ama yanıt expectedFailure:true taşır ve benchmark raporu bu satırı plansız hata saymaz: 2026-09-06 koşusunda on başarısız çağrının ALTISI kasıtlı negatif testti ve ayıklama elle yapıldı (S-4)."),
429
+ 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
+ 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
+ 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"),
435
+ 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)."),
437
+ 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
+ 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."),
278
439
  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')
279
440
  },
280
441
  annotations: readOnly
281
- }, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, anonymous, outPath }) => {
442
+ }, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, captureUnready, anonymous, asUser, cookies, layoutReport, actions, outPath, expectFailure, language, colorScheme }) => {
282
443
  try {
444
+ if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
445
+ 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
+ }
283
447
  if (!config.studioBaseUrl) {
284
448
  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).");
285
449
  }
286
450
  const resolvedEnvironment = platform.resolveEnvironment(environment);
287
- const lane = resolvedEnvironment === 'development' ? 'dev' : resolvedEnvironment === 'production' ? 'view' : undefined;
451
+ // U-04 (2026-09-09): kısıt önce gateway'de DEĞİL Studio'da olduğu için reddediliyordu —
452
+ // runtime rotası dal adı taşımıyor, ortamı modül içi bir store belirliyordu ve dal adıyla
453
+ // istenen kare development'ı gösterirdi. Studio artık `/branch/{env}/…` şeridini tanıyor ve
454
+ // ortamı URL'den okuyor (f-02); dal karesi bu yüzden GERÇEKTEN o dalın karesi. Geriye yalnız
455
+ // platformun dal diye kabul etmeyeceği adlar kalıyor — onlar için şerit kurulmaz.
456
+ const lane = resolvedEnvironment === 'development'
457
+ ? 'dev'
458
+ : resolvedEnvironment === 'production'
459
+ ? 'view'
460
+ : isBranchName(resolvedEnvironment) ? `branch/${resolvedEnvironment}` : undefined;
288
461
  if (!lane) {
289
- throw new PlatformApiError(`page_screenshot yalnız 'development' (/dev) ve 'production' (/view) ortamlarını görüntüleyebilir; '${resolvedEnvironment}' için Studio'da karşılık gelen bir rota yok.`);
462
+ throw new PlatformApiError(`page_screenshot '${resolvedEnvironment}' ortamını görüntüleyemez: bu ad ne sabit bir ortam ne de geçerli bir dal adı. Sabitler 'development' (/dev) ve 'production' (/view); dal adı bir harfle başlar, yalnız küçük harf/rakam/alt çizgi taşır (3-31 karakter) ve rezerve olamaz ('public', 'api', 'pg_*' gibi). Projedeki dalları environment_list ile görün.`);
463
+ }
464
+ const projectSlug = platform.resolveProject(project);
465
+ const languageChoice = language === undefined ? undefined : normalizeLanguageCode(language, 'language');
466
+ if (languageChoice) {
467
+ // Bağ burada kurulmazsa sessiz yanlış kare doğar: runtime tanımadığı kodu yok sayar ve
468
+ // sayfayı varsayılan dilde çizer (resolveRuntimeLanguage yalnız projenin taşıdığı kodu
469
+ // seçer), kare de 'ikinci dil' diye rapora girerdi.
470
+ const state = readProjectLanguages(await platform.org('GET', `/p/${projectSlug}`).catch(() => undefined));
471
+ if (state && state.languages.length > 0 && !state.languages.includes(languageChoice.code)) {
472
+ throw new PlatformApiError(`page_screenshot: '${languageChoice.code}' projenin dilleri arasında değil (${state.languages.join(', ')}). Runtime bilmediği kodu yok sayıp kareyi varsayılan dilde çizer — dili project_update(languages) ile ekleyin ya da listedeki bir kodu verin.`);
473
+ }
290
474
  }
291
- const url = `${config.studioBaseUrl}/${lane}/${platform.resolveProject(project)}/${applicationSlug}/${pageSlug}`;
475
+ const url = `${config.studioBaseUrl}/${lane}/${projectSlug}/${applicationSlug}/${pageSlug}${languageChoice ? `?lang=${languageChoice.code}` : ''}`;
292
476
  const size = viewport ?? { width: 1280, height: 800 };
477
+ const extraCookies = Object.entries(cookies ?? {}).map(([name, value]) => ({ name, value }));
478
+ const identity = anonymous === true ? 'anonymous' : asUser === undefined ? 'gateway' : 'asUser';
479
+ const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
293
480
  const shot = await screenshotter.capture({
294
481
  url,
295
482
  apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
296
- token: anonymous ? undefined : await platform.accessToken(),
483
+ token: anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken(),
484
+ ...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
297
485
  cookieUrl: config.studioBaseUrl,
298
486
  viewport: size,
299
487
  fullPage: fullPage === true,
300
- delayMs: delayMs ?? 0
488
+ delayMs: delayMs ?? 0,
489
+ ...(captureUnready === true ? { captureUnready: true } : {}),
490
+ ...(layoutReport === true ? { layoutReport: true } : {}),
491
+ ...(colorScheme === undefined ? {} : { colorScheme }),
492
+ ...(actions === undefined ? {} : { actions: actions })
301
493
  });
302
494
  const meta = {
303
495
  url,
304
496
  environment: resolvedEnvironment,
497
+ ...(languageChoice ? { language: languageChoice.code } : {}),
498
+ ...(languageChoice?.note ? { languageNote: languageChoice.note } : {}),
499
+ ...(colorScheme === undefined ? {} : { colorScheme }),
305
500
  viewport: size,
306
501
  fullPage: fullPage === true,
502
+ identity,
503
+ cookieNames,
307
504
  sizeBytes: shot.png.byteLength,
308
505
  elapsedMs: shot.elapsedMs,
506
+ ...(shot.contentHeight === undefined ? {} : { contentHeight: shot.contentHeight }),
507
+ ...(shot.fullPageTruncated
508
+ ? { 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.` }
509
+ : {}),
309
510
  ...(shot.apiStatus === undefined ? {} : { apiStatus: shot.apiStatus }),
310
511
  ...(shot.apiStatus !== undefined && shot.apiStatus >= 300
311
- ? { middlewareBlocked: true, note: 'Sayfanın veri isteği reddedildi — karede hata/boş durum görünüyor olabilir. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır (page_runtime_get ile karşılaştırın).' }
512
+ ? { middlewareBlocked: true, note: `Sayfanın veri isteği reddedildi — karede hata/boş durum görünüyor olabilir. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır (page_runtime_get ile karşılaştırın).${identity === 'gateway' ? " Gateway'in servis jetonu uygulamanın kendi kullanıcı tablosuna çözülmeyebilir; üye kimliğiyle çekmek için asUser (ve gerekiyorsa cookies) verin. Uygulamanın 'users' tablosu boşsa hiçbir kimlik çözülmez; yeni projede o tablo BOŞ gelir — row_list('users') ile bakın, ilk kullanıcıyı açma yolu platform_guide('getting-started') §'İlk kullanıcıyı kim açar?'." : ''}` }
513
+ : {}),
514
+ // P1-1 (S-3): eşzamanlı çekimler gateway içinde SERİ koşar; ajan yavaşlığın kuyruktan mı
515
+ // yoksa sayfadan mı geldiğini bilsin.
516
+ ...(shot.queuedMs === undefined ? {} : { queuedMs: shot.queuedMs }),
517
+ ...(shot.contextRecycled === true
518
+ ? { contextRecycled: true, contextRecycledNote: 'Önceki çekim tarayıcı bağlamını bozdu; bu kare TEMİZ bir tarayıcıyla alındı — oturum çerezleri yeniden enjekte edildi.' }
519
+ : {}),
520
+ // S-8 (09-08): soğuk Studio'da navigasyon zaman aşımı ajanın çağrıyı tekrarlamasını
521
+ // gerektiriyordu; artık gateway bir kez kendi deniyor ve karenin neden geciktiğini söylüyor.
522
+ ...(shot.navigationRetried === true
523
+ ? { navigationRetried: true, navigationRetriedNote: 'İlk navigasyon zaman aşımına düştü ve kare ikinci denemede alındı — Studio soğuktu (ilk istek Vite derlemesini ısıtır). Sonraki çekimler hızlı olmalı; olmuyorsa delayMs verin, o değer navigasyon bütçesini de uzatır.' }
524
+ : {}),
525
+ // B-6 (2026-09-07): kare yalnız sayfa hazırken alınır; hangi kanıtla hazır sayıldığı yazılır.
526
+ pageState: shot.pageState,
527
+ readyAfterMs: shot.readyAfterMs,
528
+ ...(shot.finalUrl === undefined ? {} : { finalUrl: shot.finalUrl }),
529
+ ...(shot.pageState === 'ready-no-flows'
530
+ ? { pageStateNote: 'Runtime çizildi ama hiç mount akışı koşmadı. Sayfa akışsızsa (giriş ekranı) normaldir; veri bekliyorsanız page_schema_get ile mount olayına bağlı akış olup olmadığına bakın.' }
531
+ : shot.pageState === 'ready-no-counter'
532
+ ? { pageStateNote: 'Runtime koşan akış sayacı yayınlamıyor (eski runtime); hazırlık DOM durulmasından çıkarıldı, akışların bittiği kanıtlanamadı.' }
533
+ : shot.pageState === 'error'
534
+ ? { pageStateNote: 'Studio sayfa için hata/bulunamadı durumunu çizdi; kare o durumu gösterir. apiStatus ve slug\'ları doğrulayın.' }
535
+ : shot.pageState === 'redirected'
536
+ ? { pageStateNote: `İstenen adres runtime dışına yönlendi (finalUrl). Route guard ya da Studio giriş ekranı olabilir; kare yönlendirilen sayfayı gösterir.` }
537
+ : shot.pageState === 'not-ready'
538
+ ? { pageStateNote: 'TEŞHİS KARESİ (captureUnready): bütçe dolduğunda sayfa hazır değildi; kare iskelet ya da yarım durumu gösterebilir, doğrulama için kullanmayın.' }
539
+ : {}),
540
+ ...(shot.renderWaitTimedOut && shot.pageState === 'not-ready'
541
+ ? { renderWaitTimedOut: true, note: "Sayfanın veri isteği gözlenmedi — STUDIO_BASE_URL ve slug'ları doğrulayın." }
312
542
  : {}),
313
- ...(shot.renderWaitTimedOut
314
- ? { renderWaitTimedOut: true, note: "Sayfanın veri isteği gözlenmedi — kare hata/boş durumu gösteriyor olabilir; STUDIO_BASE_URL ve slug'ları doğrulayın." }
315
- : {})
543
+ ...(shot.flowsSettled === undefined
544
+ ? {}
545
+ : shot.flowsSettled
546
+ ? { flowsSettled: true }
547
+ : { flowsSettled: false, flowsSettledNote: 'Sayfanın akışları bütçe içinde bitmedi; kare akış koşarken alındı (captureUnready). Akış gerçekten uzunsa delayMs ile bütçeyi büyütün.' }),
548
+ ...(shot.actions === undefined ? {} : { actionsReport: shot.actions }),
549
+ // S-4: kasıtlı negatif test de akış hatası bırakır ve ölçüm aracı onu plansız hata
550
+ // sayıyordu. Çağıran beklediğini söylediğinde satır ayrı sayılır.
551
+ ...(expectFailure === true ? { expectedFailure: true } : {}),
552
+ ...(shot.runtimeErrors === undefined
553
+ ? {}
554
+ : {
555
+ runtimeErrors: shot.runtimeErrors,
556
+ 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
+ }),
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
+ : {}),
565
+ ...(shot.layout === undefined
566
+ ? {}
567
+ : shot.layout.length === 0
568
+ ? { layoutReportUnavailable: true, note: "Sayfada data-element-id taşıyan düğüm yok — runtime bu attribute'u henüz basmıyor olabilir (F-04) ya da sayfa boş render edildi." }
569
+ : {
570
+ layoutSummary: summarizeLayout(shot.layout, size),
571
+ ...(shot.layout.length <= 100 ? { layout: shot.layout } : { layoutTruncatedTo: 'summary' })
572
+ })
316
573
  };
317
574
  if (outPath) {
318
575
  const absolute = resolve(outPath);
319
576
  try {
577
+ // B-6: klasör yoksa ENOENT ile düşüyordu; koşu ajanı her seferinde klasörü elle açıyordu.
578
+ await mkdir(dirname(absolute), { recursive: true });
320
579
  await writeFile(absolute, shot.png);
321
580
  }
322
581
  catch (error) {
@@ -376,10 +635,16 @@ export function registerPageTools(server, platform) {
376
635
  server.registerTool('page_schema_validate', {
377
636
  title: 'Sayfa şemasını doğrula (kuru çalıştırma)',
378
637
  description: "Micro-op'ları kaydetmeden şemaya uygular ve doğrulama hatalarını (suggestions ile) döndürür; page_schema_patch öncesi planı test etmek için. Gövdeye page_schema_patch ile AYNI normalizasyonu uygular (Prop sarmalı, validation biçimi) — kuru koşu ile yazma aynı gövdeyi görür.",
379
- inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional() },
638
+ inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional(), operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için") },
380
639
  annotations: readOnly
381
- }, handle(config, async ({ project, environment, applicationId, pageId, ops }) => {
382
- const opRecords = (ops ?? []).map(op => op);
640
+ }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations }) => {
641
+ const allOps = microOps('page_schema_validate', ops, operations).map(op => op);
642
+ const propTypeIssues = setPropTypeErrors(allOps.filter(entry => entry.op === 'set_prop_type'));
643
+ if (propTypeIssues.length > 0)
644
+ 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)));
383
648
  const normalized = normalizeSchemaOps(opRecords);
384
649
  const validationIssues = schemaOpValidationErrors(opRecords);
385
650
  const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
@@ -396,22 +661,34 @@ export function registerPageTools(server, platform) {
396
661
  }));
397
662
  server.registerTool('page_schema_patch', {
398
663
  title: 'Sayfa şemasına patch uygula',
399
- 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.",
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.",
400
665
  inputSchema: {
401
666
  project: projectParam,
402
667
  environment: environmentParam,
403
668
  applicationId: guid,
404
669
  pageId: guid,
405
- ops: opsParam,
406
- expectedUpdatedAt: z.string().optional().describe("page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz")
670
+ ops: opsParam.optional(),
671
+ 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"),
673
+ 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
+ view: writeViewParam
407
675
  },
408
676
  annotations: updates
409
- }, handle(config, async ({ project, environment, applicationId, pageId, ops, expectedUpdatedAt }) => {
410
- const allOps = ops.map(op => op);
677
+ }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, expectedUpdatedAt, allowFlowDeletion, view }) => {
678
+ const allOps = microOps('page_schema_patch', ops, operations).map(op => op);
411
679
  const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
412
- const opRecords = allOps.filter(entry => entry.op !== 'remove_event');
680
+ const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
681
+ const propTypeOps = allOps.filter(entry => entry.op === 'set_prop_type');
682
+ 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);
413
686
  const containerOps = opRecords
414
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);
691
+ }
415
692
  if (containerOps.length > 0) {
416
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);
417
694
  }
@@ -455,23 +732,27 @@ export function registerPageTools(server, platform) {
455
732
  if (unknown.length > 0)
456
733
  throw new PlatformApiError(`PAGE-008 — geçersiz olay adı (istek gönderilmedi): ${unknown.join(' · ')}`, 400);
457
734
  }
735
+ // Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
736
+ // silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
737
+ let deletedFlows = [];
738
+ if (opRecords.some(entry => ['remove_component', 'set_event', 'remove_event'].includes(String(entry.op)))) {
739
+ try {
740
+ const page = await readPage();
741
+ deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
742
+ }
743
+ catch {
744
+ deletedFlows = [];
745
+ }
746
+ if (deletedFlows.length > 0 && allowFlowDeletion !== true) {
747
+ throw new PlatformApiError(orphanedFlowMessage(deletedFlows), 400);
748
+ }
749
+ }
458
750
  const added = opRecords
459
751
  .filter(entry => entry.op === 'add_component')
460
752
  .map(entry => entry.component);
461
- const badWidgets = invalidWidgetTypeNodes(added, 'PAGE-012');
462
- if (badWidgets.length > 0)
463
- throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
464
- const badNames = invalidComponentNames(added);
465
- if (badNames.length > 0) {
466
- throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badNames.join(' · ')}. `
467
- + 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; boşluklu ya da geçersiz bir ad '
468
- + 'SAYFADAKİ TÜM ifadeleri "Missing initializer in const declaration" ile öldürür (layout adları her sayfaya sızar). '
469
- + 'name alanını elementId ile aynı yaz; okunabilir Türkçe başlık için props.title / props.text / props.label kullan.', 400);
470
- }
471
- const catalog = added.length > 0
472
- ? await applyCatalogLayoutRules(platform, project, environment, applicationId, added)
473
- : { clamped: [], heights: [] };
474
- const formWarnings = formBindingWarnings(opRecords);
753
+ assertScreenWriteGuards('page', { schema: added });
754
+ const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, added);
755
+ const formWarnings = [...formBindingWarnings(opRecords), ...formSubmitButtonWarnings(opRecords)];
475
756
  let flowWarnings = [];
476
757
  let layoutWarnings = [];
477
758
  if (opRecords.some(entry => entry.op === 'remove_component')) {
@@ -482,6 +763,16 @@ export function registerPageTools(server, platform) {
482
763
  catch {
483
764
  flowWarnings = [];
484
765
  }
766
+ // Ters yön (FLOW-018): silinen düğüme YAZAN setter'lar. Akış gövdeleri şemada değil, ayrı
767
+ // uçta; okunamazsa uyarı üretmeyiz ama yama yine geçer.
768
+ try {
769
+ const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
770
+ const page = await readPage();
771
+ flowWarnings = [...flowWarnings, ...staleSetterWarnings(hosted?.data ?? [], opRecords, page.schema)];
772
+ }
773
+ catch {
774
+ // akış listesi okunamadı
775
+ }
485
776
  }
486
777
  let nestedAfterOps = [];
487
778
  const simulatable = opRecords.some(entry => ['add_component', 'move_component'].includes(String(entry.op))
@@ -498,7 +789,8 @@ export function registerPageTools(server, platform) {
498
789
  ...pageContainerPositionWarning(page.schema),
499
790
  ...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
500
791
  ...stringStyleWarning(stringStyleNodes(simulated)),
501
- ...inertFlexPropWarning(inertFlexPropNodes(simulated))
792
+ ...inertFlexPropWarning(inertFlexPropNodes(simulated)),
793
+ ...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')
502
794
  ];
503
795
  }
504
796
  catch {
@@ -512,29 +804,62 @@ export function registerPageTools(server, platform) {
512
804
  : [];
513
805
  if (removeEventOps.length > 0)
514
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;
515
817
  const response = opRecords.length > 0
516
818
  ? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: opRecords } })) ?? {})
517
819
  : { valid: true };
820
+ if (view !== 'full')
821
+ outlineWriteResponse(response);
518
822
  if (removedEvents.length > 0)
519
823
  response.removedEvents = removedEvents;
824
+ if (propTypes.length > 0)
825
+ response.propTypes = propTypes;
826
+ if (resized.applied.length > 0)
827
+ response.resized = resized.applied;
828
+ if (resized.warnings.length > 0)
829
+ response.resizeWarnings = resized.warnings;
830
+ if (deletedFlows.length > 0)
831
+ response.deletedFlows = deletedFlows;
520
832
  if (formWarnings.length > 0)
521
833
  response.formWarnings = formWarnings;
522
834
  if (flowWarnings.length > 0)
523
835
  response.flowWarnings = flowWarnings;
524
836
  if (layoutWarnings.length > 0)
525
837
  response.layoutWarnings = layoutWarnings;
526
- return withNormalization(response, [
838
+ // G-23 micro-op ayağı: yama 200 döner ve `appliedPatch` op'un ŞEMAYA yazıldığını söyler, ama
839
+ // alanın gerçekten KAYDEDİLDİĞİNİ söylemez. Yamadan sonra şemayı bir kez okuyup op'un yazmak
840
+ // 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');
842
+ let opDrops = [];
843
+ if (contentOps.length > 0) {
844
+ try {
845
+ const saved = await fetchPage(platform, project, environment, applicationId, pageId);
846
+ opDrops = schemaOpDrops(saved.schema, contentOps);
847
+ }
848
+ catch {
849
+ opDrops = [];
850
+ }
851
+ }
852
+ return withWriteDiff(withNormalization(response, [
527
853
  ...rootAddsNote(retargeted, PAGE_CONTAINER_ELEMENT_ID),
528
854
  ...componentDefaultsNote(normalized.components),
529
855
  ...validationNote(normalized.validations),
530
- ...blockSizeNote(catalog.clamped),
531
- ...heightTypeNote(catalog.heights),
856
+ ...catalogNotes,
532
857
  ...replacePropNote(normalized.props)
533
- ]);
858
+ ]), opDrops);
534
859
  }));
535
860
  server.registerTool('page_create', {
536
861
  title: 'Sayfa oluştur',
537
- description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (doğrulama duvarı + canlı canvas orada). roleIds için role_list, layoutId için layout_list kullanın.",
862
+ description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (canlı canvas ve hedefli düzeltme orada). Şemayı burada gönderirseniz 2026-09-08'den beri AYNI doğrulama duvarından geçer: tanınmayan prop adı ve geçersiz enum değeri 422 ile reddedilir ve sayfa HİÇ açılmaz. roleIds için role_list, layoutId için layout_list kullanın. Yanıttaki schema VARSAYILAN OLARAK iskelettir (çözülmüş layout düğümleri dâhil id/type/children'a iner) — tam gövde için view:'full'; gönderdiğiniz bir alan kaydedilmediyse silentlyDropped söyler.",
538
863
  inputSchema: {
539
864
  project: projectParam,
540
865
  environment: environmentParam,
@@ -542,30 +867,24 @@ export function registerPageTools(server, platform) {
542
867
  name: z.string().min(1),
543
868
  slug: z.string().min(1).describe("URL yolu, örn. 'orders' ya da 'orders/detail'"),
544
869
  browserTitle: z.string().optional(),
870
+ browserTitleKey: z.string().optional().describe("Sekme başlığının çeviri anahtarı. Verilirse başlık çeviri sözlüğünden çözülür ve language-selector ile dil değişince sayfa yenilenmeden güncellenir; boşsa browserTitle olduğu gibi kullanılır. Anahtarlar için translation_list."),
545
871
  layoutId: guid.optional(),
546
872
  roleIds: z.array(guid).optional().describe('Sayfayı görebilecek roller; boş = herkese açık'),
547
873
  middlewareIds: z.array(guid).optional(),
548
874
  events: eventsParam.optional(),
549
- schema: schemaParam.optional()
875
+ schema: schemaParam.optional(),
876
+ view: writeViewParam
550
877
  },
551
878
  annotations: writes
552
- }, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, layoutId, roleIds, middlewareIds, events, schema }) => {
553
- const badEvents = invalidScreenEventNames(events, 'PAGE-011');
554
- if (badEvents.length > 0)
555
- throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
879
+ }, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, events, schema, view }) => {
556
880
  const container = ensurePageContainer(schema ?? []);
557
- const badWidgets = invalidWidgetTypeNodes(container.schema, 'PAGE-012');
558
- if (badWidgets.length > 0)
559
- throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
560
- const nested = nestedRepeaterNodes(container.schema);
561
- if (nested.length > 0)
562
- throw new PlatformApiError(nestedRepeaterMessage(nested), 400);
881
+ assertScreenWriteGuards('page', { schema: container.schema, events });
563
882
  const stringStyles = stringStyleNodes(container.schema);
564
883
  const fixed = normalizeComponentDefaults(container.schema);
565
884
  const reset = resetPageContainerPosition(container.schema);
566
- const catalog = (schema ?? []).length > 0
567
- ? await applyCatalogLayoutRules(platform, project, environment, applicationId, container.schema)
568
- : { clamped: [], heights: [] };
885
+ const catalogNotes = (schema ?? []).length > 0
886
+ ? await screenCatalogNotes(platform, project, environment, applicationId, container.schema)
887
+ : [];
569
888
  const moved = reorderSchemaBlocks(container.schema);
570
889
  const gaps = (schema ?? []).length > 0 ? verticalGapWarnings(container.schema, 'PAGE-010 (uyarı)') : [];
571
890
  const response = await platform.tenant('POST', project, `/pages/${applicationId}`, {
@@ -574,6 +893,7 @@ export function registerPageTools(server, platform) {
574
893
  name,
575
894
  slug,
576
895
  browserTitle: browserTitle ?? '',
896
+ browserTitleKey: browserTitleKey ?? '',
577
897
  layoutId: layoutId ?? null,
578
898
  roleIds: roleIds ?? [],
579
899
  middlewareIds: middlewareIds ?? [],
@@ -581,8 +901,20 @@ export function registerPageTools(server, platform) {
581
901
  schema: container.schema
582
902
  }
583
903
  });
584
- const normalized = withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...blockSizeNote(catalog.clamped), ...heightTypeNote(catalog.heights), ...reorderNote(moved)]);
585
- const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema))];
904
+ const saved = (response && typeof response === 'object' && !Array.isArray(response)
905
+ ? (response.data ?? response)
906
+ : {});
907
+ // Kırpmadan ÖNCE doğrulama yolu (B-55): yanıt gövdesi iskelete inince "gönderdiğim prop
908
+ // kaydedildi mi" sorusunu gövde değil bu diff cevaplar. `layout_create`'in yaptığının sayfa
909
+ // ikizi; Prop sarmalı açılmazsa doğru kaydedilmiş her prop düşmüş görünür (G-28).
910
+ const drops = (schema ?? []).length > 0
911
+ ? silentlyDropped({ schema: unwrapProps(container.schema) }, { schema: unwrapProps(saved.schema) })
912
+ : [];
913
+ if (view !== 'full' && Array.isArray(saved.schema)) {
914
+ saved.schema = saved.schema.map(node => summarize(node));
915
+ }
916
+ 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ı)')];
586
918
  if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
587
919
  return normalized;
588
920
  return { ...normalized, layoutWarnings: createWarnings };
@@ -598,6 +930,7 @@ export function registerPageTools(server, platform) {
598
930
  name: z.string().min(1).optional(),
599
931
  slug: z.string().min(1).optional(),
600
932
  browserTitle: z.string().optional(),
933
+ browserTitleKey: z.string().optional().describe("Sekme başlığının çeviri anahtarı. Verilirse başlık çeviri sözlüğünden çözülür ve language-selector ile dil değişince sayfa yenilenmeden güncellenir; boşsa browserTitle olduğu gibi kullanılır. Anahtarlar için translation_list."),
601
934
  layoutId: guid.nullable().optional().describe('null: layout bağını kaldırır'),
602
935
  roleIds: z.array(guid).optional(),
603
936
  middlewareIds: z.array(guid).optional(),
@@ -607,31 +940,15 @@ export function registerPageTools(server, platform) {
607
940
  annotations: updates
608
941
  }, handle(config, async ({ project, environment, applicationId, pageId, ...changes }) => {
609
942
  const overrides = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== undefined));
610
- const badEvents = invalidScreenEventNames(overrides.events, 'PAGE-011');
611
- if (badEvents.length > 0)
612
- throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
613
- const badWidgets = invalidWidgetTypeNodes(overrides.schema, 'PAGE-012');
614
- if (badWidgets.length > 0)
615
- throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
616
- const nestedUpdate = nestedRepeaterNodes(overrides.schema);
617
- if (nestedUpdate.length > 0)
618
- throw new PlatformApiError(nestedRepeaterMessage(nestedUpdate), 400);
943
+ assertScreenWriteGuards('page', { schema: overrides.schema, events: overrides.events });
619
944
  const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
620
- const badSchemaNames = invalidComponentNames(overrides.schema);
621
- if (badSchemaNames.length > 0) {
622
- throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badSchemaNames.join(' · ')}. `
623
- + 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; geçersiz bir ad sayfadaki TÜM ifadeleri öldürür. '
624
- + 'name = elementId yazın.', 400);
625
- }
626
945
  const fixed = normalizeComponentDefaults(overrides.schema);
627
946
  const reset = overrides.schema === undefined ? [] : resetPageContainerPosition(overrides.schema);
628
- const catalog = overrides.schema === undefined
629
- ? { clamped: [], heights: [] }
630
- : await applyCatalogLayoutRules(platform, project, environment, applicationId, overrides.schema);
947
+ const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, overrides.schema);
631
948
  const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
632
949
  const gaps = overrides.schema === undefined
633
950
  ? []
634
- : [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema))];
951
+ : [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema)), ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
635
952
  const body = pageUpdateBody(current, overrides);
636
953
  await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
637
954
  const summary = {
@@ -641,7 +958,7 @@ export function registerPageTools(server, platform) {
641
958
  };
642
959
  if (gaps.length > 0)
643
960
  summary.layoutWarnings = gaps;
644
- return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...blockSizeNote(catalog.clamped), ...heightTypeNote(catalog.heights), ...reorderNote(moved)]);
961
+ return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]);
645
962
  }));
646
963
  server.registerTool('page_clone', {
647
964
  title: 'Sayfa klonla',