@appaflytech/zeyrek-ai-mcp 0.0.36 → 0.0.40

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 (210) hide show
  1. package/.env.example +15 -0
  2. package/README.md +5 -5
  3. package/dist/config.js +37 -0
  4. package/dist/index.js +2 -0
  5. package/dist/knowledge/access.js +802 -0
  6. package/dist/knowledge/backendConstraints.js +18 -1
  7. package/dist/knowledge/case.js +41 -0
  8. package/dist/knowledge/componentCatalog.js +524 -53
  9. package/dist/knowledge/componentEvents.js +132 -7
  10. package/dist/knowledge/connectDataBind.js +851 -0
  11. package/dist/knowledge/connectDataDefinition.js +344 -0
  12. package/dist/knowledge/connectDataExecute.js +93 -0
  13. package/dist/knowledge/connectDataRules.js +128 -0
  14. package/dist/knowledge/decision.js +121 -0
  15. package/dist/knowledge/expressionPrelude.js +60 -0
  16. package/dist/knowledge/layoutSummary.js +140 -2
  17. package/dist/knowledge/microserviceParameters.js +58 -0
  18. package/dist/knowledge/mining.js +94 -0
  19. package/dist/knowledge/mobileComponentCatalog.js +876 -0
  20. package/dist/knowledge/mobileComponentEvents.js +259 -0
  21. package/dist/knowledge/mobileStore.js +396 -0
  22. package/dist/knowledge/normalize.js +240 -72
  23. package/dist/knowledge/outlines.js +36 -0
  24. package/dist/knowledge/platform/lint.js +385 -0
  25. package/dist/knowledge/platform/mobile.js +142 -0
  26. package/dist/knowledge/platform/mobileLayoutLint.js +324 -0
  27. package/dist/knowledge/platform/mobileRules.js +231 -0
  28. package/dist/knowledge/platform/mobileSpacing.js +82 -0
  29. package/dist/knowledge/platform/mobileTabs.js +108 -0
  30. package/dist/knowledge/platform/nativeBoxModel.js +44 -0
  31. package/dist/knowledge/platform/profile.js +25 -0
  32. package/dist/knowledge/platform/resolve.js +122 -0
  33. package/dist/knowledge/platform/web.js +39 -0
  34. package/dist/knowledge/policy.js +18 -4
  35. package/dist/knowledge/process.js +510 -0
  36. package/dist/knowledge/reservedTables.js +79 -0
  37. package/dist/knowledge/rowFilter.js +43 -0
  38. package/dist/knowledge/rules.js +485 -22
  39. package/dist/knowledge/schemaGuard.js +11 -0
  40. package/dist/knowledge/styleResolve.js +64 -10
  41. package/dist/knowledge/styleResolveNative.js +529 -0
  42. package/dist/knowledge/themeResolve.js +3 -3
  43. package/dist/observability/trace.js +13 -5
  44. package/dist/platform/argumentAliases.js +128 -0
  45. package/dist/platform/bodyFile.js +103 -0
  46. package/dist/platform/capture/actions.js +398 -0
  47. package/dist/platform/capture/browser.js +1 -0
  48. package/dist/platform/capture/capture.js +683 -0
  49. package/dist/platform/capture/console-log.js +122 -0
  50. package/dist/platform/capture/index.js +11 -0
  51. package/dist/platform/capture/readiness.js +103 -0
  52. package/dist/platform/capture/summary.js +77 -0
  53. package/dist/platform/capture/target.js +5 -0
  54. package/dist/platform/capture/targets/studio-mobile.js +15 -0
  55. package/dist/platform/capture/targets/studio-web.js +5 -0
  56. package/dist/platform/diagnostics.js +10 -4
  57. package/dist/platform/domSummary.js +1 -1
  58. package/dist/platform/errors.js +163 -10
  59. package/dist/platform/http.js +29 -8
  60. package/dist/platform/impersonation.js +45 -0
  61. package/dist/platform/jsonArray.js +24 -0
  62. package/dist/platform/responses.js +39 -5
  63. package/dist/platform/schemaReadMemory.js +60 -0
  64. package/dist/platform/secrets.js +68 -2
  65. package/dist/platform/strictArguments.js +68 -12
  66. package/dist/platform/tokenVault.js +187 -4
  67. package/dist/resources/examples/bpm-gorev-kontrol.json +294 -0
  68. package/dist/resources/examples/bpm-iso-dokuman.json +228 -0
  69. package/dist/resources/examples/bpm-satinalma.json +376 -0
  70. package/dist/resources/examples/connect-data-convert-verify.json +107 -0
  71. package/dist/resources/examples/connect-data-dropdown-roles.json +66 -0
  72. package/dist/resources/examples/connect-data-edit-m2m.json +73 -0
  73. package/dist/resources/examples/connect-data-query-params.json +50 -0
  74. package/dist/resources/examples/connect-data-table-entity.json +147 -0
  75. package/dist/resources/examples/connect-data-workflow-microservice.json +65 -0
  76. package/dist/resources/examples/middleware-authenticate.json +1 -1
  77. package/dist/resources/examples/mobile-app-shell-drawer.json +244 -0
  78. package/dist/resources/examples/mobile-app-shell-stack.json +157 -0
  79. package/dist/resources/examples/mobile-app-shell-tab.json +208 -0
  80. package/dist/resources/examples/mobile-form-screen.json +365 -0
  81. package/dist/resources/examples/mobile-list-detail-form.json +703 -0
  82. package/dist/resources/examples/page-detail-profile.json +2 -1
  83. package/dist/resources/examples/page-dynamic-form.json +3 -2
  84. package/dist/resources/examples/page-flex-sections.json +1 -1
  85. package/dist/resources/examples/page-html-embed.json +1 -1
  86. package/dist/resources/examples/workflow-basic.json +8 -2
  87. package/dist/resources/guides/access.md +810 -0
  88. package/dist/resources/guides/bpm.md +691 -0
  89. package/dist/resources/guides/case.md +153 -0
  90. package/dist/resources/guides/component.md +92 -5
  91. package/dist/resources/guides/decision.md +185 -0
  92. package/dist/resources/guides/entity.md +50 -8
  93. package/dist/resources/guides/expressions.md +14 -5
  94. package/dist/resources/guides/functions.md +1 -1
  95. package/dist/resources/guides/getting-started.md +38 -1
  96. package/dist/resources/guides/layout.md +11 -7
  97. package/dist/resources/guides/merge.md +42 -7
  98. package/dist/resources/guides/microservice.md +88 -3
  99. package/dist/resources/guides/middleware.md +69 -5
  100. package/dist/resources/guides/mobile-layout.md +193 -0
  101. package/dist/resources/guides/mobile-publish.md +148 -0
  102. package/dist/resources/guides/mobile.md +565 -0
  103. package/dist/resources/guides/page.md +469 -82
  104. package/dist/resources/guides/theme.md +8 -2
  105. package/dist/resources/guides/translation.md +1 -1
  106. package/dist/resources/guides/workflow.md +58 -1
  107. package/dist/resources/read.js +41 -0
  108. package/dist/resources/rules.js +11 -5
  109. package/dist/resources/schema-fingerprint.json +7 -5
  110. package/dist/resources/schemas/AccessExplanationDto.json +144 -0
  111. package/dist/resources/schemas/AccessMatrixDto.json +170 -0
  112. package/dist/resources/schemas/AccessPermissionDto.json +57 -0
  113. package/dist/resources/schemas/AccessPolicyDto.json +223 -0
  114. package/dist/resources/schemas/AccessResourceDto.json +86 -0
  115. package/dist/resources/schemas/AppSessionDto.json +53 -0
  116. package/dist/resources/schemas/ConnectDataDescribeDto.json +344 -0
  117. package/dist/resources/schemas/ConnectDataRequestModel.json +202 -0
  118. package/dist/resources/schemas/ConnectDataResultDto.json +421 -0
  119. package/dist/resources/schemas/ConnectorCredentialDto.json +70 -0
  120. package/dist/resources/schemas/ConvertConnectDataToMicroserviceCommand.json +37 -0
  121. package/dist/resources/schemas/CreateAccessPolicyCommand.json +209 -0
  122. package/dist/resources/schemas/CreateDecisionCommand.json +221 -0
  123. package/dist/resources/schemas/CreateEntityCommand.json +118 -0
  124. package/dist/resources/schemas/CreateMicroserviceCommand.json +13 -1
  125. package/dist/resources/schemas/CreateProcessDefinitionCommand.json +1337 -0
  126. package/dist/resources/schemas/CreateQueryCommand.json +4 -0
  127. package/dist/resources/schemas/CreateWorkflowCommand.json +146 -1
  128. package/dist/resources/schemas/DataJobDto.json +6 -1
  129. package/dist/resources/schemas/DecisionDto.json +302 -0
  130. package/dist/resources/schemas/DetachConnectDataMicroserviceCommand.json +28 -0
  131. package/dist/resources/schemas/ExplainAccessQuery.json +33 -0
  132. package/dist/resources/schemas/GenerateProcessDocumentCommand.json +116 -0
  133. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +24 -0
  134. package/dist/resources/schemas/MicroserviceDto.json +15 -3
  135. package/dist/resources/schemas/OrganizationProjectDto.json +343 -0
  136. package/dist/resources/schemas/OrganizationSchemaDto.json +196 -0
  137. package/dist/resources/schemas/OrganizationSchemaVersionDto.json +207 -0
  138. package/dist/resources/schemas/PatchProcessDefinitionCommand.json +76 -0
  139. package/dist/resources/schemas/PreviewMicroserviceCommand.json +10 -1
  140. package/dist/resources/schemas/PreviewWorkflowCommand.json +25 -1
  141. package/dist/resources/schemas/ProcessDefinitionDto.json +1480 -0
  142. package/dist/resources/schemas/ProcessSimulationDto.json +96 -0
  143. package/dist/resources/schemas/SaveConnectorCredentialCommand.json +90 -0
  144. package/dist/resources/schemas/SaveProcessEventSourceCommand.json +61 -0
  145. package/dist/resources/schemas/SaveProcessMailSourceCommand.json +89 -0
  146. package/dist/resources/schemas/SignProcessTaskCommand.json +73 -0
  147. package/dist/resources/schemas/SimulateProcessDefinitionQuery.json +92 -0
  148. package/dist/resources/schemas/StartMobileBuildCommand.json +54 -0
  149. package/dist/resources/schemas/StepDto.json +25 -1
  150. package/dist/resources/schemas/UpdateAccessPolicyCommand.json +209 -0
  151. package/dist/resources/schemas/UpdateDecisionCommand.json +216 -0
  152. package/dist/resources/schemas/UpdateEntityCommand.json +118 -0
  153. package/dist/resources/schemas/UpdateMicroserviceCommand.json +13 -1
  154. package/dist/resources/schemas/UpdateProcessDefinitionCommand.json +1357 -0
  155. package/dist/resources/schemas/UpdateQueryCommand.json +4 -0
  156. package/dist/resources/schemas/UpdateViewCommand.json +0 -2
  157. package/dist/resources/schemas/UpdateWorkflowCommand.json +146 -1
  158. package/dist/resources/schemas/UpsertMobileStoreSettingsCommand.json +333 -0
  159. package/dist/resources/schemas/ValidateProcessDefinitionQuery.json +1339 -0
  160. package/dist/server.js +31 -1
  161. package/dist/tools/access.js +369 -2
  162. package/dist/tools/accessOperations.js +135 -0
  163. package/dist/tools/accessPolicies.js +170 -0
  164. package/dist/tools/accessRuntime.js +203 -0
  165. package/dist/tools/applications.js +63 -3
  166. package/dist/tools/bpm.js +343 -0
  167. package/dist/tools/bpmGovernance.js +266 -0
  168. package/dist/tools/bpmOperations.js +230 -0
  169. package/dist/tools/bpmReporting.js +154 -0
  170. package/dist/tools/bpmRouting.js +150 -0
  171. package/dist/tools/bpmRuntime.js +396 -0
  172. package/dist/tools/case.js +178 -0
  173. package/dist/tools/caseRuntime.js +89 -0
  174. package/dist/tools/common.js +19 -6
  175. package/dist/tools/components.js +118 -14
  176. package/dist/tools/connectData.js +450 -0
  177. package/dist/tools/connectors.js +193 -0
  178. package/dist/tools/datasources.js +3 -3
  179. package/dist/tools/decisions.js +206 -0
  180. package/dist/tools/dependencies.js +1 -1
  181. package/dist/tools/deployments.js +38 -3
  182. package/dist/tools/designSchemaPatch.js +34 -9
  183. package/dist/tools/dtos.js +36 -5
  184. package/dist/tools/entities.js +79 -12
  185. package/dist/tools/environments.js +1 -1
  186. package/dist/tools/externalComponents.js +26 -5
  187. package/dist/tools/flows.js +119 -13
  188. package/dist/tools/folders.js +1 -1
  189. package/dist/tools/imports.js +5 -2
  190. package/dist/tools/layouts.js +50 -17
  191. package/dist/tools/merges.js +2 -2
  192. package/dist/tools/microservices.js +77 -25
  193. package/dist/tools/mobile.js +280 -0
  194. package/dist/tools/mobileStore.js +572 -0
  195. package/dist/tools/modules.js +48 -0
  196. package/dist/tools/organizationStructure.js +137 -0
  197. package/dist/tools/pages.js +467 -100
  198. package/dist/tools/platformInfo.js +28 -8
  199. package/dist/tools/profileWrite.js +92 -0
  200. package/dist/tools/projects.js +71 -9
  201. package/dist/tools/rows.js +69 -13
  202. package/dist/tools/schemaOps.js +61 -1
  203. package/dist/tools/screenWrite.js +36 -7
  204. package/dist/tools/screens.js +95 -5
  205. package/dist/tools/templates.js +26 -1
  206. package/dist/tools/translations.js +1 -1
  207. package/dist/tools/variables.js +11 -5
  208. package/dist/tools/workflows.js +10 -5
  209. package/package.json +2 -2
  210. package/dist/platform/screenshot.js +0 -1162
@@ -1,28 +1,39 @@
1
- import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive, microOps, isBranchName } from './common.js';
1
+ import { guid, projectParam, environmentParam, pagingParams, asUserIdParam, readOnly, writes, updates, destructive, microOps, isBranchName, branchSlug } from './common.js';
2
2
  import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEventOps, stripGatewayEventFields, staleSetterWarnings, lostSetterTargets, fillComponentNames, componentNameNote, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, codePropTemplateErrors, addComponentIdErrors, missingBlocksNodes, assertAddComponentIds, assertComponentBlocks, 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, findNode, flowPath } from './screens.js';
4
+ import { attachFindings } from '../knowledge/policy.js';
5
+ import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode, flowPath, countSchemaNodes, missingElementIds, splitSchemaIntoParts, encodeSchemaCursor, decodeSchemaCursor } from './screens.js';
5
6
  import { fetchComponentEventNames } from './components.js';
6
- import { assertComponentNames, assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
7
- import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, planEventRemovals, resizeServerOps, planResizes, planPropTypes, rejectedOpsMessage } from './schemaOps.js';
7
+ import { assertComponentNames, assertScreenWriteGuards, screenCatalogNotes, assertScreenCatalog } from './screenWrite.js';
8
+ import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, styleTargetTypes, simulateLayoutOps, applySkeletonOps, reshapesSkeleton, planEventRemovals, resizeServerOps, planResizes, planPropTypes, rejectedOpsMessage } from './schemaOps.js';
8
9
  import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
9
10
  import { emptyWriteDiff, schemaOpDrops, unwrapProps, withWriteDiff, writeDiff } from '../platform/writeDiff.js';
10
11
  import { PlatformApiError } from '../platform/errors.js';
11
12
  import { existingMatch, ifExistsParam, listAll, withOutcome } from '../platform/ifExists.js';
12
- import { handle, toolResult, toolError } from '../platform/responses.js';
13
- import { StudioScreenshotter } from '../platform/screenshot.js';
13
+ import { handle, toolResult, toolError, prune } from '../platform/responses.js';
14
+ import { expressionErrorsNote, runtimeErrorsNote, StudioScreenshotter } from '../platform/capture/index.js';
15
+ import { captureTargetFor } from '../platform/capture/target.js';
14
16
  import { diffSummaries, summaryLines, summaryStats } from '../platform/domSummary.js';
15
17
  import { captureKey, imageHash, sharedCaptureMemory } from '../platform/screenshotMemory.js';
18
+ import { pageReadKey, schemaReadKey, sharedSchemaReadMemory } from '../platform/schemaReadMemory.js';
16
19
  import { summarizeLayout } from '../knowledge/layoutSummary.js';
17
20
  import { normalizeLanguageCode } from '../knowledge/languageCodes.js';
18
21
  import { readProjectLanguages } from '../knowledge/languages.js';
19
- import { resolveIdentityToken } from '../platform/tokenVault.js';
22
+ import { resolveIdentity } from '../platform/tokenVault.js';
23
+ import { impersonationHeaders } from '../platform/impersonation.js';
24
+ import { accessDecisionOf, legacyRoleListFindings, projectAccessContext } from '../knowledge/access.js';
20
25
  import { mkdir, writeFile } from 'node:fs/promises';
21
26
  import { dirname, resolve } from 'node:path';
22
27
  import { setPropTypeErrors } from '../knowledge/propTypes.js';
28
+ import { connectDataOpIssues, connectedPropKeys } from '../knowledge/connectDataDefinition.js';
23
29
  import { boxSectionNote, collectIds, ensureBoxSections, expandSectionToggles } from '../knowledge/boxSections.js';
24
30
  import { designSnapshotInput } from '../knowledge/designSnapshot.js';
25
31
  import { discoverPage } from '../knowledge/pageDiscover.js';
32
+ import { applicationTypeField, resolveProfile, resolveProfileBySlug } from '../knowledge/platform/resolve.js';
33
+ import { resolveThemeNative } from '../knowledge/styleResolveNative.js';
34
+ import { boundFlowSteps, pageLayoutWarnings, screenProfileWarnings } from './profileWrite.js';
35
+ import { applyMobileSpacingDefaults, applyMobileSpacingToOps, emptyMobileSpacingFix, mobileSpacingNote } from '../knowledge/platform/mobileSpacing.js';
36
+ import { shellLessPageSkeleton } from '../knowledge/platform/mobile.js';
26
37
  import { z } from 'zod';
27
38
  /**
28
39
  * Runtime paketindeki `appFunctions`/`pageFunctions` (UiFunctionRuntimeDto: id, name, parameters,
@@ -57,10 +68,6 @@ async function orderByIndex(ops, readPage) {
57
68
  return { ops, moved: [], warnings: [] };
58
69
  }
59
70
  }
60
- /** Şema ağacındaki TÜM düğümler (kök + iç içe çocuklar); page_get ve page_create özeti aynı sayımı kullanır. */
61
- function countSchemaNodes(nodes) {
62
- return (nodes ?? []).reduce((total, node) => total + 1 + countSchemaNodes(node.children), 0);
63
- }
64
71
  async function fetchPage(platform, project, environment, applicationId, pageId) {
65
72
  const response = await platform.tenant('GET', project, `/pages/${applicationId}/${pageId}`, { environment });
66
73
  return response?.data ?? {};
@@ -73,13 +80,44 @@ async function fetchPage(platform, project, environment, applicationId, pageId)
73
80
  */
74
81
  export const SERVER_PAGE_OPS = [
75
82
  'replace_prop', 'set_binding', 'set_prop_type', 'add_component', 'remove_component', 'move_component', 'reorder', 'set_block',
76
- 'set_appearance', 'set_states', 'set_event', 'add_event', 'remove_event', 'set_validations', 'set_state_binding', 'remove_state_binding', 'batch'
83
+ 'set_appearance', 'set_states', 'set_event', 'add_event', 'remove_event', 'set_validations', 'set_state_binding', 'remove_state_binding', 'set_connect_data',
84
+ 'remove_connect_data', 'batch'
77
85
  ];
78
- export function registerPageTools(server, platform, injectedScreenshotter, injectedMemory) {
86
+ export function registerPageTools(server, platform, injectedScreenshotter, injectedMemory, injectedSchemaReadMemory) {
79
87
  const config = platform.config;
80
- 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.");
81
- /** Sayfa ve uç araçları aynı kimlik sözleşmesini paylaşır (G-20); çözüm tokenVault'ta yaşar. */
82
- const resolveIdentity = resolveIdentityToken;
88
+ const schemaReadMemory = injectedSchemaReadMemory ?? sharedSchemaReadMemory;
89
+ const readKey = (project, environment, pageId, version) => schemaReadKey(project ?? config.defaultProject ?? '', environment ?? config.defaultEnvironment, pageId, String(version ?? ''));
90
+ const pageKey = (project, environment, pageId) => pageReadKey(project ?? config.defaultProject ?? '', environment ?? config.defaultEnvironment, pageId);
91
+ const rememberVersion = (project, environment, pageId, version) => {
92
+ if ((typeof version === 'string' && version.length > 0) || typeof version === 'number') {
93
+ schemaReadMemory.rememberVersion(pageKey(project, environment, pageId), String(version));
94
+ }
95
+ };
96
+ /**
97
+ * g-05: sayfa sürüm çakışması (409 `PageSchemaVersionConflictException`) ajana "sayfa hangi
98
+ * sürümde, kim ne zaman değiştirdi" ile döner. Güncel sürüm sayfadan, son yazma revizyon
99
+ * defterinden (`/revisions/{projectId}`, en yeni tek satır) okunur; ikisi de okunamazsa mesaj
100
+ * o parçasız gider — çeviri hiçbir zaman çakışmanın kendisini gizlemez. Başka her hata olduğu
101
+ * gibi döner.
102
+ */
103
+ const versionConflict = async (error, project, environment, applicationId, pageId, sentVersion, nextStep) => {
104
+ if (!(error instanceof PlatformApiError) || error.code !== 'PageSchemaVersionConflictException')
105
+ return error;
106
+ const currentVersion = await fetchPage(platform, project, environment, applicationId, pageId).then(page => page.version, () => undefined);
107
+ const revision = await (async () => {
108
+ const projectPayload = await platform.org('GET', `/p/${platform.resolveProject(project)}`);
109
+ const revisions = await platform.org('GET', `/revisions/${projectPayload?.data?.id ?? ''}`, {
110
+ query: { environment: environment ?? config.defaultEnvironment, module: 'page', recordId: pageId, pageLength: 1 }
111
+ });
112
+ return revisions?.data?.[0];
113
+ })().catch(() => undefined);
114
+ const actor = (revision?.actor ?? {});
115
+ const lastWrite = revision
116
+ ? ` — son yazma ${String(revision.createdAt ?? '?')}, ${String(actor.name ?? actor.id ?? 'bilinmeyen kullanıcı')}, kaynak ${String(revision.source ?? '?')}, revizyon ${String(revision.revisionNo ?? '?')}`
117
+ : '';
118
+ return new PlatformApiError(`Sayfa siz okuduktan sonra değişti: gönderilen sürüm ${sentVersion ?? '(yok)'}, güncel sürüm ${String(currentVersion ?? '?')}${lastWrite}. HİÇBİR ŞEY yazılmadı. ${nextStep}`, 409, error.code);
119
+ };
120
+ 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. KOMPLEKS PROJEDE referans access_session_create'ten gelir ve page_runtime_get'te Bearer olarak gider (CSRF gerekmez); page_screenshot Studio arayüzünü açtığı için uygulama jetonuyla çekim kompleks projede canlı doğrulanmadı. ÜÇ ŞEKİL kabul edilir: (1) TERCİH EDİLEN `test:<ad>` — gateway kök .env'deki MCP_TEST_USER_<AD> satırının tarif ettiği giriş ucunu kendisi çağırır ve jetonu kasadan yeniden kullanır (satırı insan yazar; tanımsızsa hata eklenecek değişkeni söyler), (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 (ömür MCP_TOKEN_TTL_MINUTES; MCP_STATE_DIR varsa gateway yeniden başlayınca da çözülür), (3) ham JWT. anonymous ile birlikte verilemez.");
83
121
  server.registerTool('page_list', {
84
122
  title: 'Sayfaları listele',
85
123
  description: 'Uygulamanın sayfalarını (id, name, slug) listeler; hedef sayfayı bulmak için ilk adım.',
@@ -101,12 +139,14 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
101
139
  }));
102
140
  server.registerTool('page_get', {
103
141
  title: 'Sayfa üstbilgisini getir',
104
- description: "Sayfanın kimlik ve bağ bilgisi: slug, browserTitle, layoutId, middlewareIds, roleIds, sayfa düzeyi olaylar (events[].value = flow id) ve flow özetleri. ŞEMA DÖNDÜRMEZ — şema için page_schema_get. 'Sayfa middleware'e bağlı mı, hangi roller görüyor, hangi olaya hangi akış bağlı' sorularının cevabı budur (Kural 8 denetimi).",
142
+ description: "Sayfanın kimlik ve bağ bilgisi: slug, browserTitle, layoutId, middlewareIds, roleIds, sayfa düzeyi olaylar (events[].value = flow id) ve flow özetleri. ŞEMA DÖNDÜRMEZ — şema için page_schema_get. 'Sayfa middleware'e bağlı mı, hangi roller görüyor, hangi olaya hangi akış bağlı' sorularının cevabı budur (Kural 8 denetimi). Yanıttaki version gateway'de hatırlanır: ardından gelen page_update onu baseVersion olarak gönderir (PAGE-039).",
105
143
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
106
144
  annotations: readOnly
107
145
  }, handle(config, async ({ project, environment, applicationId, pageId }) => {
108
146
  const page = await fetchPage(platform, project, environment, applicationId, pageId);
147
+ const resolved = await resolveProfile(platform, applicationId);
109
148
  const layout = (page.layout ?? {});
149
+ rememberVersion(project, environment, pageId, page.version);
110
150
  return {
111
151
  id: page.id,
112
152
  name: page.name,
@@ -124,12 +164,14 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
124
164
  flows: (Array.isArray(page.flows) ? page.flows : [])
125
165
  .map(flow => ({ id: flow.id, name: flow.name, slug: flow.slug })),
126
166
  schemaNodes: countSchemaNodes(page.schema),
127
- updatedAt: page.audit?.updatedAt ?? page.audit?.createdAt
167
+ version: page.version,
168
+ updatedAt: page.audit?.updatedAt ?? page.audit?.createdAt,
169
+ ...applicationTypeField(resolved)
128
170
  };
129
171
  }));
130
172
  server.registerTool('page_runtime_get', {
131
173
  title: 'Sayfanın runtime çıktısını getir',
132
- 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.",
174
+ 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. Kompleks projede yanıta 'accessDecision' eklenir (allow | deny | not-enforced | unknown, mode, izin, reason, next): sayfa reddi page.view kararından mı geldi, sıradaki adım ne.",
133
175
  inputSchema: {
134
176
  project: projectParam,
135
177
  environment: environmentParam,
@@ -138,33 +180,38 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
138
180
  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'),
139
181
  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."),
140
182
  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."),
141
- asUser: asUserParam
183
+ asUser: asUserParam,
184
+ asUserId: asUserIdParam
142
185
  },
143
186
  annotations: readOnly
144
- }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous, asUser }) => {
187
+ }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous, asUser, asUserId }) => {
145
188
  let response;
146
189
  if (anonymous === true && asUser !== undefined) {
147
190
  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.');
148
191
  }
149
192
  // page_screenshot çerez, bu tool başlık taşıyordu; çerez okuyan middleware ikincisinden
150
193
  // yapısal olarak geçemiyordu. Kimlik artık iki kanalda birden gider (Y-02).
151
- const identity = asUser === undefined ? {} : { headers: { authorization: `Bearer ${resolveIdentity(asUser)}` } };
194
+ const eye = await impersonationHeaders(platform, 'page_runtime_get', project, environment, { asUserId, asUser, anonymous }, { runtime: true });
195
+ const identity = asUser === undefined ? (eye ? { headers: eye } : {}) : { headers: { authorization: `Bearer ${await resolveIdentity(asUser, platform, project, environment)}` } };
152
196
  try {
153
197
  response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous, mirrorAuthorizationCookie: true, ...identity });
154
198
  }
155
199
  catch (error) {
156
200
  if (error instanceof PlatformApiError && typeof error.status === 'number' && error.status >= 300) {
201
+ const decision = accessDecisionOf(await projectAccessContext(platform, project, environment), error.status, error.code, error.apiMessage);
157
202
  return {
158
203
  middlewareBlocked: true,
159
204
  status: error.status,
160
205
  message: error.message,
206
+ ...(decision ? { accessDecision: decision } : {}),
161
207
  note: "Sayfaya bağlı middleware isteği reddetti ya da kayıt bulunamadı. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır. Sayfanın bağlarını page_get ile, middleware tarifini platform_guide('middleware') ile kontrol edin; slug'ları page_list/application_list doğrular."
162
208
  };
163
209
  }
164
210
  throw error;
165
211
  }
212
+ const decision = accessDecisionOf(await projectAccessContext(platform, project, environment), 200);
166
213
  if (view === 'full')
167
- return response;
214
+ return decision ? { ...response, accessDecision: decision } : response;
168
215
  const body = (response?.data ?? {});
169
216
  const layout = body.layout;
170
217
  const theme = body.project?.theme;
@@ -172,11 +219,19 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
172
219
  const styleSource = resolveStyles ? (theme ?? null) : undefined;
173
220
  const asNodes = (value) => (Array.isArray(value) ? value : []);
174
221
  const named = (value) => (Array.isArray(value) ? value : []).map(entry => ({ id: entry.id, name: entry.name, slug: entry.slug }));
222
+ const runtimeApplication = (body.application ?? {});
223
+ // Mobil uygulama React Native stiliyle çizilir (G4): rapor CSS kaskadı değil `shell`/`text`.
224
+ const nativeTheme = resolveStyles && runtimeApplication.type === 'mobile' ? resolveThemeNative((theme ?? undefined)) : undefined;
225
+ // Mobilde de tema katmanı (componentStyles ← $preset) çizerin tabanıdır (f-16); rapor aynı kaynağı okur.
226
+ const nodeStyleSource = styleSource;
175
227
  return {
228
+ ...(decision ? { accessDecision: decision } : {}),
176
229
  page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, browserTitleKey: body.browserTitleKey, isHomepage: body.isHomepage },
230
+ ...(typeof runtimeApplication.type === 'string' ? { applicationType: runtimeApplication.type } : {}),
177
231
  events: body.events ?? [],
178
- layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(node => summarize(node, styleSource)) } : null,
179
- schema: asNodes(body.schema).map(node => summarize(node, styleSource)),
232
+ layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(node => summarize(node, nodeStyleSource, nativeTheme)) } : null,
233
+ schema: asNodes(body.schema).map(node => summarize(node, nodeStyleSource, nativeTheme)),
234
+ ...(nativeTheme ? { styleResolver: 'native' } : {}),
180
235
  widgets: named(body.widgets),
181
236
  flows: named(body.flows),
182
237
  variableKeys: Object.keys((body.variables ?? {})),
@@ -195,9 +250,11 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
195
250
  }
196
251
  }
197
252
  : {}),
198
- note: resolveStyles
199
- ? "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."
200
- : "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."
253
+ note: nativeTheme
254
+ ? "Mobil uygulama: nativeStyle, mobil çizerin düğüme verdiği React Native stilidir — shell kutunun, text içine akan metnin stili (RN'de tipografi miras alınmaz); warnings mobilde çizilmeyen değerlerdir (MOB-002). Tema componentStyles ← $preset katmanı TABANDIR (panel grupları üstüne biner, serbest style son söz); mobil katalogda style prop'u olmadığı için $preset'e yeni başvuru yazılamaz, componentStyles tipin bütün düğümlerine iner."
255
+ : resolveStyles
256
+ ? "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."
257
+ : "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."
201
258
  };
202
259
  }));
203
260
  const screenshotter = injectedScreenshotter ?? new StudioScreenshotter(config);
@@ -205,7 +262,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
205
262
  const maxInlinePngBytes = 5 * 1024 * 1024;
206
263
  server.registerTool('page_screenshot', {
207
264
  title: 'Sayfanın ekran görüntüsünü al',
208
- description: "ÖNCE KARESİZ DENE: 'kart göründü mü, tablo doldu mu, hata var mı' gibi olgu sorularında image:'none' verin — kare çekilmez, yanıttaki summaryText her kök düğümün görünürlüğünü, metnini, satır sayısını, açık sekmesini ve hata mesajlarını söyler (bir tam sayfa karesinin yüzlerce katı küçük). Kareyi yalnız görsel soru (renk, hizalama, referansa sadakat) için alın ve küçültün: clip ile bölge, scale:0.5 ile yarı çözünürlük; aynı sayfayı yeniden çekerken image:'ifChanged'. 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).",
265
+ description: "ÖNCE KARESİZ DENE: 'kart göründü mü, tablo doldu mu, hata var mı' gibi olgu sorularında image:'none' verin — kare çekilmez, yanıttaki summaryText her kök düğümün görünürlüğünü, metnini, satır sayısını, açık sekmesini ve hata mesajlarını söyler (bir tam sayfa karesinin yüzlerce katı küçük). Kareyi yalnız görsel soru (renk, hizalama, referansa sadakat) için alın ve küçültün: clip ile bölge, scale:0.5 ile yarı çözünürlük; aynı sayfayı yeniden çekerken image:'ifChanged'. 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. Bir komponent prop'unun İFADESİ patlarsa (Studio'da 'Expression error' toast'ı) yanıt expressionErrors taşır: {elementId, prop, expression, message, count}, öğe başına en çok iki; aynı satırın konsol kopyası consoleErrors'a düşmez. Sayfa ya da bir bileşen ÇİZİLEMEZSE neden yanıttadır: mobilde çöken düğüm renderErrors ({elementId, message}; sayfanın geri kalanı çizilir), yakalanmamış istisna pageErrors, konsol hatası consoleErrors — kare alınamadıysa bütün nedenler hata metninde. KARE DONUKTUR: tarayıcı bağlamı 'hareketi azalt' tercihiyle açılır; mobil runtime bileşen hareketini (giriş, döngü, geçiş, iskelet parıltısı, yükleme göstergesi) durdurur ve aynı sayfanın iki karesi tekrarlanabilir. Web sayfasında yalnız CSS'i prefers-reduced-motion'a uyan animasyonlar durur. Hareketin kendisini görmek için motion:'play'. 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).",
209
266
  inputSchema: {
210
267
  project: projectParam,
211
268
  environment: environmentParam,
@@ -214,14 +271,23 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
214
271
  viewport: z.object({
215
272
  width: z.number().int().min(320).max(3840),
216
273
  height: z.number().int().min(240).max(2160)
217
- }).optional().describe('Varsayılan 1280x800 (deviceScaleFactor 1)'),
274
+ }).optional().describe('Varsayılan 1280x800 (deviceScaleFactor 1); mobil uygulamada varsayılan device. Verilirse device yok sayılır'),
275
+ device: z.union([
276
+ z.enum(['phone', 'tablet']),
277
+ z.object({
278
+ width: z.number().int().min(320).max(3840),
279
+ height: z.number().int().min(240).max(2160),
280
+ scale: z.number().min(1).max(3).optional()
281
+ })
282
+ ]).optional().describe("Yalnız MOBİL uygulama: cihaz ön ayarı — 'phone' 390x844 @2x (varsayılan), 'tablet' 820x1180 @2x, ya da { width, height, scale? }. Kare cihaz piksel yoğunluğunda çıkar (phone → 780x1688 px); raster küçültme için scale parametresi bununla çarpılır. Web uygulamasında reddedilir: dar ekran için viewport verin."),
218
283
  fullPage: z.boolean().optional().describe('true: kaydırılan kısım dahil tüm sayfa; varsayılan yalnız viewport karesi'),
219
284
  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)."),
220
285
  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."),
221
286
  anonymous: z.boolean().optional().describe("true: hiçbir çerez enjekte edilmez — anonim ziyaretçinin gördüğü kare. Korumalı sayfada middlewareBlocked bekleyin."),
222
287
  asUser: asUserParam,
288
+ asUserId: asUserIdParam.describe("Parolasız kullanıcı gözüyle kare (users satır id'si): tarayıcı gateway jetonuyla açılır ve sayfanın BÜTÜN istekleri 'impersonated-user-id' başlığını taşır — Studio önizlemesindeki 'View as user' seçicisinin yaptığının aynısı. Kompleks enforce projede gateway jetonu tek başına 401 alır; kullanıcı gözüyle kare bu alanla alınır. Yalnız development ya da dal (production'da çağrı reddedilir); asUser/anonymous/cookies ile birlikte verilemez."),
223
289
  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)."),
224
- 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."),
290
+ 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. Mobil uygulamada özet ayrıca layoutSummary.alignment taşır: gutter (ekran kenarı boşluğu), leftEdges (aynı kapsamdaki geniş içeriğin sol kenar kümeleri — sözleşmede tek küme), surfaces (iç hizası bozuk yüzey kapları), widthOutliers (sağ kenarı sapan geniş öğeler, içeri itilme dp'si), offScaleGaps (0/4/8/16/24/32 dışı dikey aralıklar). Yan yana dizilen, ekran dışında kalan ve akış dışı (app-bar, fab, modal…) öğeler kıyasa girmez. Düğümler data-element-id taşımıyorsa layoutReportUnavailable döner."),
225
291
  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."),
226
292
  actions: z.array(z.object({
227
293
  action: z.enum(['set', 'click', 'wait', 'drag', 'tab', 'select', 'submit']),
@@ -230,8 +296,9 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
230
296
  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"),
231
297
  ms: z.number().int().min(0).max(15000).optional().describe("'wait' için bekleme süresi")
232
298
  })).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. Autocomplete'te 'set' metni alana yazar, 'select' metni yazıp öneri satırını GÖRÜNEN METİNLE seçer (value ile değil; eşleşme yoksa yazılan metin alanda kalır ve note bunu 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)."),
233
- 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ı."),
299
+ 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ı. MOBİL uygulamada da geçerlidir (/preview aynı ?lang'i okur); cihazın kendisinde ?lang yoktur (platform_guide('mobile') §Çok dil)."),
234
300
  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."),
301
+ motion: z.enum(['frozen', 'play']).optional().describe("Varsayılan 'frozen': bağlam 'hareketi azalt' (prefers-reduced-motion: reduce) ile açılır; mobil runtime hareketi dondurur ve kare tekrarlanabilir olur — diff'in imageUnchanged'ı ancak böyle anlamlıdır. Web sayfasında yalnız bu tercihe uyan CSS animasyonları durur. 'play': hareket oynar (giriş animasyonu, dönen gösterge, iskelet parıltısı); kare çekildiği ana bağlıdır, iki kare bayt bayt aynı çıkmayabilir ve yanıt bunu motionNote ile söyler. diff belleği iki kipi ayrı tutar."),
235
302
  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."),
236
303
  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."),
237
304
  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."),
@@ -240,12 +307,12 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
240
307
  z.object({ x: z.number(), y: z.number(), width: z.number().positive(), height: z.number().positive() })
241
308
  ]).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."),
242
309
  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."),
243
- 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."),
310
+ 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, hareket kipi, viewport, fullPage, actions, clip, scale ve biçimdir — asUser karesi gateway karesiyle karşılaştırılmaz."),
244
311
  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."),
245
312
  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')
246
313
  },
247
314
  annotations: readOnly
248
- }, 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 }) => {
315
+ }, async ({ project, environment, applicationSlug, pageSlug, viewport, device, fullPage, delayMs, captureUnready, anonymous, asUser, asUserId, cookies, layoutReport, actions, outPath, expectFailure, language, colorScheme, motion, summary, image, format, quality, scale, clip, diff }) => {
249
316
  try {
250
317
  if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
251
318
  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.");
@@ -273,9 +340,9 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
273
340
  ? 'dev'
274
341
  : resolvedEnvironment === 'production'
275
342
  ? 'view'
276
- : isBranchName(resolvedEnvironment) ? `branch/${resolvedEnvironment}` : undefined;
343
+ : isBranchName(resolvedEnvironment) ? `branch/${branchSlug(resolvedEnvironment)}` : undefined;
277
344
  if (!lane) {
278
- 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.`);
345
+ 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, küçük harf/rakam/alt çizgi/tire ve '/' ile ayrılmış parçalar taşır (3-31 karakter, ör. feature/odeme) ve rezerve olamaz ('public', 'api', 'pg_*' gibi). Dal şeridi adın '/' yerine '-' taşıyan hâliyle (slug) kurulur. Projedeki dalları environment_list ile görün.`);
279
346
  }
280
347
  const projectSlug = platform.resolveProject(project);
281
348
  const languageChoice = language === undefined ? undefined : normalizeLanguageCode(language, 'language');
@@ -288,16 +355,38 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
288
355
  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.`);
289
356
  }
290
357
  }
358
+ if (asUserId !== undefined && cookies !== undefined) {
359
+ throw new PlatformApiError("page_screenshot: asUserId ile cookies birlikte verilemez — uygulama oturum çerezi (ZY_App_…) geçerliyse platform başlığı değil çerezin sahibini özne yapar ve karenin kimin gözüyle çekildiği belirsiz kalır.");
360
+ }
361
+ const eye = await impersonationHeaders(platform, 'page_screenshot', project, environment, { asUserId, asUser, anonymous }, { runtime: true });
291
362
  const url = `${config.studioBaseUrl}/${lane}/${projectSlug}/${applicationSlug}/${pageSlug}${languageChoice ? `?lang=${languageChoice.code}` : ''}`;
292
- const size = viewport ?? { width: 1280, height: 800 };
363
+ // Hedef ve cihaz uygulamanın platform profilinden (G3): mobil uygulama react-native-web
364
+ // çizimidir, varsayılan kare telefon ön ayarıdır. Tip okunamazsa web davranışı aynen.
365
+ const resolved = await resolveProfileBySlug(platform, projectSlug, applicationSlug);
366
+ const target = captureTargetFor(resolved.profile.capture.target);
367
+ const mobile = resolved.profile.platform === 'mobile';
368
+ if (device !== undefined && !mobile) {
369
+ throw new PlatformApiError(`page_screenshot: device yalnız MOBİL uygulamada geçerlidir — '${applicationSlug}' ${resolved.known ? 'bir web uygulaması' : 'uygulamasının tipi okunamadı (web varsayıldı)'}. Web'in dar ekran kırılımını görmek için viewport verin (ör. { width: 390, height: 844 }).`);
370
+ }
371
+ const deviceName = typeof device === 'string' ? device : device === undefined && mobile ? resolved.profile.capture.defaultDevice : undefined;
372
+ const preset = typeof device === 'object'
373
+ ? { width: device.width, height: device.height, dpr: device.scale ?? 1 }
374
+ : deviceName === undefined ? undefined : resolved.profile.capture.devices[deviceName];
375
+ const appliedDevice = viewport === undefined && preset !== undefined ? preset : undefined;
376
+ const deviceNote = viewport !== undefined && device !== undefined
377
+ ? 'viewport verildiği için device yok sayıldı: kare verilen viewport ve deviceScaleFactor 1 ile çekildi.'
378
+ : undefined;
379
+ const size = viewport ?? (appliedDevice ? { width: appliedDevice.width, height: appliedDevice.height } : { width: 1280, height: 800 });
380
+ const dpr = appliedDevice && appliedDevice.dpr !== 1 ? appliedDevice.dpr : undefined;
293
381
  const extraCookies = Object.entries(cookies ?? {}).map(([name, value]) => ({ name, value }));
294
- const identity = anonymous === true ? 'anonymous' : asUser === undefined ? 'gateway' : 'asUser';
295
- const identityToken = anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken();
382
+ const identity = anonymous === true ? 'anonymous' : asUser !== undefined ? 'asUser' : eye ? 'asUserId' : 'gateway';
383
+ const identityToken = anonymous ? undefined : await resolveIdentity(asUser, platform, project, environment) ?? await platform.accessToken();
296
384
  const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
297
385
  const shot = await screenshotter.capture({
298
386
  url,
299
387
  apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
300
388
  token: identityToken,
389
+ ...(eye ? { headers: eye } : {}),
301
390
  ...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
302
391
  cookieUrl: config.studioBaseUrl,
303
392
  viewport: size,
@@ -306,6 +395,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
306
395
  ...(captureUnready === true ? { captureUnready: true } : {}),
307
396
  ...(layoutReport === true ? { layoutReport: true } : {}),
308
397
  ...(colorScheme === undefined ? {} : { colorScheme }),
398
+ ...(motion === 'play' ? { motion } : {}),
309
399
  // B-2: `summary:false` + `diff` verildiğinde özet yine toplanır — fark ona dayanır ve
310
400
  // toplanmazsa bellek hiç yazılmaz, `ifChanged` sessizce ölü bir özellik olurdu.
311
401
  ...(summary === false && !(diff === true || image === 'ifChanged') ? { summary: false } : {}),
@@ -314,7 +404,9 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
314
404
  ...(quality === undefined ? {} : { quality }),
315
405
  ...(scale === undefined ? {} : { scale }),
316
406
  ...(clip === undefined ? {} : { clip: clip }),
317
- ...(actions === undefined ? {} : { actions: actions })
407
+ ...(actions === undefined ? {} : { actions: actions }),
408
+ ...(mobile ? { target } : {}),
409
+ ...(dpr === undefined ? {} : { dpr })
318
410
  });
319
411
  // Fark ÖZETTEN çıkar; `summary:false` verilse de fark için toplanmıştır (yanıta girmez).
320
412
  // Anahtar KİMLİĞİ ayırır (K8): 'asUser' etiketi yetmez — iki farklı üyenin jetonu aynı
@@ -324,10 +416,12 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
324
416
  const memoryKey = captureKey({
325
417
  url,
326
418
  identity,
327
- principal: identityToken,
419
+ // Aynı gateway jetonu altında iki farklı asUserId'nin karesi karşılaştırılmasın (B-3 dersinin aynısı).
420
+ principal: asUserId === undefined ? identityToken : `${identityToken ?? ''}|${asUserId}`,
328
421
  cookieValues: extraCookies.map(cookie => `${cookie.name}=${cookie.value}`),
329
422
  language: languageChoice?.code,
330
423
  colorScheme,
424
+ motion: motion ?? 'frozen',
331
425
  viewport: size,
332
426
  fullPage: fullPage === true,
333
427
  actions,
@@ -384,7 +478,13 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
384
478
  ...(languageChoice ? { language: languageChoice.code } : {}),
385
479
  ...(languageChoice?.note ? { languageNote: languageChoice.note } : {}),
386
480
  ...(colorScheme === undefined ? {} : { colorScheme }),
481
+ ...(motion === 'play' ? { motion, motionNote: 'Hareket açık: kare çekildiği ana bağlıdır, aynı sayfanın iki karesi bayt bayt aynı çıkmayabilir (diff imageUnchanged hareketi ölçmez). Tekrarlanabilir kare için motion verilmez.' } : {}),
387
482
  viewport: size,
483
+ ...(appliedDevice === undefined ? {} : { device: typeof device === 'object' ? { ...device } : deviceName, deviceScaleFactor: appliedDevice.dpr }),
484
+ ...(deviceNote === undefined ? {} : { deviceNote }),
485
+ // Web karesinin yanıtı profil öncesiyle aynı kalır (G3 kısıtı); alan yalnız mobilde.
486
+ ...(mobile ? applicationTypeField(resolved) : {}),
487
+ ...(target.platformNote === undefined ? {} : { platformNote: target.platformNote }),
388
488
  fullPage: fullPage === true,
389
489
  identity,
390
490
  cookieNames,
@@ -429,7 +529,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
429
529
  : {}),
430
530
  ...(shot.apiStatus === undefined ? {} : { apiStatus: shot.apiStatus }),
431
531
  ...(shot.apiStatus !== undefined && shot.apiStatus >= 300
432
- ? { 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?'." : ''}` }
532
+ ? { 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 (kompleks enforce projede 401 AppSessionRequired); kullanıcı gözüyle çekmek için parolasız asUserId (users satır id'si, development/dal) ya da oturum açılmış üye 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?'." : ''}` }
433
533
  : {}),
434
534
  // P1-1 (S-3): eşzamanlı çekimler gateway içinde SERİ koşar; ajan yavaşlığın kuyruktan mı
435
535
  // yoksa sayfadan mı geldiğini bilsin.
@@ -447,7 +547,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
447
547
  readyAfterMs: shot.readyAfterMs,
448
548
  ...(shot.finalUrl === undefined ? {} : { finalUrl: shot.finalUrl }),
449
549
  ...(shot.pageState === 'ready-no-flows'
450
- ? { 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.' }
550
+ ? { pageStateNote: 'Runtime çizildi ama hiç mount akışı koşmadı. Sayfa akışsızsa (giriş ekranı) ya da verisi veri kaynağı bağlantısından geliyorsa (connectData prop akış koşturmadan okunur; summaryText satırları gösterir) normaldir; veri bekliyor ve göremiyorsanız page_schema_get ile mount olayına bağlı akışa, bağlı prop için connect_data_get / connect_data_execute (op list) ile bağlantıya bakın.' }
451
551
  : shot.pageState === 'ready-no-counter'
452
552
  ? { 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ı.' }
453
553
  : shot.pageState === 'error'
@@ -473,15 +573,26 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
473
573
  ? {}
474
574
  : {
475
575
  runtimeErrors: shot.runtimeErrors,
476
- 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.`
576
+ runtimeErrorsNote: runtimeErrorsNote(shot.runtimeErrors)
477
577
  }),
578
+ ...(shot.expressionErrors === undefined
579
+ ? {}
580
+ : { expressionErrors: shot.expressionErrors, expressionErrorsNote: expressionErrorsNote(shot.expressionErrors) }),
581
+ ...(shot.renderErrors === undefined
582
+ ? {}
583
+ : {
584
+ renderErrors: shot.renderErrors,
585
+ renderErrorsNote: `${shot.renderErrors.length} düğüm çizilemedi: sayfanın geri kalanı çizildi, bu düğümlerin yerinde hata yer tutucusu var. message bileşen tipini ve hatayı söyler; genelde prop değeri ya da bileşenin kendisi (ARC kaydı).`
586
+ }),
587
+ ...(shot.pageErrors === undefined ? {} : { pageErrors: shot.pageErrors }),
588
+ ...(shot.consoleErrors === undefined ? {} : { consoleErrors: shot.consoleErrors }),
478
589
  ...(actionNotes.length === 0 ? {} : { actionsNote: actionNotes.join('\n') }),
479
590
  ...(shot.layout === undefined
480
591
  ? {}
481
592
  : shot.layout.length === 0
482
593
  ? { 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." }
483
594
  : {
484
- layoutSummary: summarizeLayout(shot.layout, size),
595
+ layoutSummary: summarizeLayout(shot.layout, size, { alignment: mobile }),
485
596
  ...(shot.layout.length <= 100 ? { layout: shot.layout } : { layoutTruncatedTo: 'summary' })
486
597
  })
487
598
  };
@@ -524,31 +635,73 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
524
635
  });
525
636
  server.registerTool('page_schema_get', {
526
637
  title: 'Sayfa şemasını oku',
527
- 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.",
638
+ 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. full ve subtree yanıtı nodeCount taşır. full tek yanıta sığmazsa HATA döner (sessiz kırpma yok): cursor:'0' ile parça parça okuyun — her parça items[] ({ parentElementId, index, node }) taşır, kalem parentElementId düğümünün children[index]'inin yerine geçer (parentElementId yoksa kök dizinin [index]'i), alt ağacı sığmayan düğüm kabuk gelir (children: [{ elementId, deferred: true }]) ve sonraki parçalar onu doldurur; nextCursor bitene kadar sürün. allowTruncated:true kırpılmış gövdeyi açıkça verir (truncated: true; hangi düğümlerin düştüğü söylenmez) — o gövdeyi page_update'e geri yazmayın (PAGE-043).",
528
639
  inputSchema: {
529
640
  project: projectParam,
530
641
  environment: environmentParam,
531
642
  applicationId: guid,
532
643
  pageId: guid,
533
- mode: modeParam,
534
- componentId: z.string().optional().describe("subtree kipinde kök alınacak komponentin elementId'si")
644
+ mode: modeParam.describe("summary (varsayılan): yalnız id/elementId/name/type ağacı — hedef bulmak için bunu kullanın. subtree: componentId ile tek alt ağaç. full: tüm şema; tek yanıta sığmazsa HATA döner — cursor:'0' ile parça parça okuyun (nextCursor bitene kadar) ya da allowTruncated:true ile kırpılmış gövdeyi açıkça isteyin."),
645
+ componentId: z.string().optional().describe("subtree kipinde kök alınacak komponentin elementId'si"),
646
+ cursor: z.string().min(1).optional().describe("Yalnız mode:'full': parçalı okuma. İlk çağrıda '0', sonra yanıttaki nextCursor; nextCursor gelmeyince şema bitmiştir. Sayfa okuma arasında değişirse 409 döner — cursor:'0' ile baştan başlayın."),
647
+ allowTruncated: z.boolean().optional().describe("Yalnız mode:'full': true verilirse sığmayan şema hata yerine kırpılarak döner (truncated: true, nodeCount toplamı söyler, düşen düğümler söylenmez). Varsayılan false: sığmayan şema hata verir.")
535
648
  },
536
649
  annotations: readOnly
537
- }, handle(config, async ({ project, environment, applicationId, pageId, mode, componentId }) => {
650
+ }, handle(config, async ({ project, environment, applicationId, pageId, mode, componentId, cursor, allowTruncated }) => {
538
651
  const page = await fetchPage(platform, project, environment, applicationId, pageId);
652
+ const typeField = applicationTypeField(await resolveProfile(platform, applicationId));
539
653
  const updatedAt = page.audit?.updatedAt ?? page.audit?.createdAt;
540
654
  const version = page.version;
541
- if (mode === 'full')
542
- return { version, updatedAt, schema: page.schema ?? [] };
655
+ rememberVersion(project, environment, pageId, version);
656
+ if (cursor !== undefined && mode !== 'full') {
657
+ throw new PlatformApiError(`cursor yalnız mode:'full' ile çalışır (verilen mode: '${mode ?? 'summary'}'); parçalı okuma tüm şema içindir. mode:'full' ile cursor:'0' verin ya da cursor'ı kaldırın.`, 400);
658
+ }
659
+ if (mode === 'full') {
660
+ const schema = page.schema ?? [];
661
+ const nodeCount = countSchemaNodes(schema);
662
+ const versionKey = String(version ?? '');
663
+ if (cursor !== undefined) {
664
+ const decoded = decodeSchemaCursor(cursor);
665
+ if (!decoded)
666
+ throw new PlatformApiError(`cursor '${cursor}' çözülemedi; cursor yalnız '0' ya da önceki yanıttaki nextCursor olabilir. cursor:'0' ile baştan başlayın.`, 400);
667
+ if (decoded.version !== undefined && decoded.version !== versionKey) {
668
+ throw new PlatformApiError(`Sayfa bu cursor alındıktan sonra değişti (cursor sürümü '${decoded.version}', güncel sürüm '${versionKey}'); eski parçalarla yenileri birleştirilirse şema tutarsız olur. Toplanan parçaları atın ve cursor:'0' ile baştan okuyun.`, 409);
669
+ }
670
+ const parts = splitSchemaIntoParts(schema, Math.floor(config.maxResponseBytes * 0.75));
671
+ const items = parts[decoded.partIndex];
672
+ if (!items)
673
+ throw new PlatformApiError(`cursor '${cursor}' ${decoded.partIndex}. parçayı istiyor ama şema ${parts.length} parça; son parçadan sonra nextCursor gelmez. cursor:'0' ile baştan başlayın.`, 400);
674
+ const nextIndex = decoded.partIndex + 1;
675
+ return {
676
+ version,
677
+ updatedAt,
678
+ ...typeField,
679
+ nodeCount,
680
+ part: { index: decoded.partIndex, count: parts.length, nodes: countSchemaNodes(items.map(item => item.node)) },
681
+ items,
682
+ ...(nextIndex < parts.length ? { nextCursor: encodeSchemaCursor(versionKey, nextIndex) } : {})
683
+ };
684
+ }
685
+ const full = { version, updatedAt, ...typeField, nodeCount, schema };
686
+ const chars = (JSON.stringify(prune(full)) ?? '').length;
687
+ if (chars > config.maxResponseBytes) {
688
+ if (allowTruncated !== true) {
689
+ const kb = (value) => (value / 1024).toFixed(1);
690
+ throw new PlatformApiError(`Şema ${nodeCount} düğüm / ${kb(chars)} KB; tek yanıta sığmaz (bütçe ${kb(config.maxResponseBytes)} KB). Kırpılmış şema hangi düğümlerin düştüğünü söylemez ve geri yazılırsa sayfa eksilir. cursor:'0' ile parça parça okuyun ya da allowTruncated:true verin (kırpılır, hangi düğümlerin düştüğü söylenmez). Hedef bulmak için mode:'summary' yeter.`, 400);
691
+ }
692
+ schemaReadMemory.markTruncated(readKey(project, environment, pageId, version));
693
+ }
694
+ return full;
695
+ }
543
696
  if (mode === 'subtree') {
544
697
  if (!componentId)
545
698
  throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
546
699
  const subtree = findNode(page.schema, componentId);
547
700
  if (!subtree)
548
701
  throw new PlatformApiError(`Sayfada '${componentId}' id'li bir komponent yok; page_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
549
- return { version, updatedAt, schema: subtree };
702
+ return { version, updatedAt, ...typeField, nodeCount: countSchemaNodes([subtree]), schema: subtree };
550
703
  }
551
- return { version, updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
704
+ return { version, updatedAt, ...typeField, schema: (page.schema ?? []).map(node => summarize(node)) };
552
705
  }));
553
706
  server.registerTool('page_state', {
554
707
  title: 'Sayfa durumunu getir',
@@ -569,7 +722,12 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
569
722
  // Kuru koşu ile yazma AYNI gövdeyi görür (P0-2 Step 3): resize_component burada da sunucu
570
723
  // op'larına çevrilir, platform dokunulan düğümleri aynı kapsamla doğrular.
571
724
  const opRecords = allOps.filter(entry => entry.op !== 'resize_component');
572
- const normalized = normalizeSchemaOps(opRecords);
725
+ const resolved = await resolveProfile(platform, applicationId);
726
+ // ARC-143: kuru koşu page_schema_patch'in add_component kapısını (PAGE-009 · 011 · 012 · 013)
727
+ // atlıyordu; ayrılmış adlı komponent validate'ten geçip yamada reddediliyordu.
728
+ assertScreenWriteGuards('page', { schema: opRecords.filter(entry => entry.op === 'add_component').map(entry => entry.component) }, resolved.profile.platform);
729
+ const readPage = () => fetchPage(platform, project, environment, applicationId, pageId);
730
+ const normalized = normalizeSchemaOps(opRecords, await styleTargetTypes(opRecords, readPage));
573
731
  const added = opRecords.filter(entry => entry.op === 'add_component').map(entry => entry.component).filter(Boolean);
574
732
  // page_schema_patch'in sırası: kimlik ve blocks op başına (PAGE-037), sonra ad (PAGE-009).
575
733
  assertAddComponentIds(opRecords);
@@ -579,7 +737,27 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
579
737
  ...schemaOpValidationErrors(opRecords).map(issue => `PAGE-016 — ${issue}`),
580
738
  ...codePropTemplateErrors(opRecords).map(issue => `PAGE-032 — ${issue}`)
581
739
  ];
582
- const ordering = await orderByIndex(opRecords, () => fetchPage(platform, project, environment, applicationId, pageId));
740
+ const spacing = await mobileSpacingForOps(resolved.profile.platform, opRecords, () => fetchPage(platform, project, environment, applicationId, pageId));
741
+ const ordering = resolved.profile.layout.orderBy === 'block-position'
742
+ ? await orderByIndex(opRecords, () => fetchPage(platform, project, environment, applicationId, pageId))
743
+ : { ops: opRecords, moved: [], warnings: [] };
744
+ const writesStyle = resolved.profile.platform !== 'web' && opRecords.some(entry => (entry.op === 'replace_prop' || entry.op === 'set_binding') && entry.prop === 'style');
745
+ const profileWarnings = await screenProfileWarnings(platform, resolved, project, environment, applicationId, {
746
+ kind: 'page',
747
+ ops: opRecords,
748
+ savedNodes: writesStyle ? await fetchPage(platform, project, environment, applicationId, pageId).then(page => page.schema, () => undefined) : undefined
749
+ });
750
+ // page_schema_patch'in Connect Data duvarı kuru koşuda da koşar: patch'in reddedeceği op burada görünür.
751
+ const connectedProps = opRecords.some(entry => ['set_binding', 'set_prop_type', 'replace_prop', 'set_connect_data', 'remove_connect_data'].includes(String(entry.op)))
752
+ ? await fetchPage(platform, project, environment, applicationId, pageId).then(page => connectedPropKeys(page.schema), () => null)
753
+ : null;
754
+ const connectDataRejections = opRecords.flatMap((entry, index) => connectDataOpIssues(entry, connectedProps).map(issue => ({
755
+ index,
756
+ op: String(entry.op ?? ''),
757
+ target: typeof entry.target === 'string' ? entry.target : null,
758
+ code: issue.code,
759
+ message: issue.message
760
+ })));
583
761
  // Yazmadaki gibi katalog kuralları GİDEN listeye: kuru koşu kırpılmamış boyutu doğrulayıp
584
762
  // yazmanın geçireceği gövdeye hata veriyor, notu da vermiyordu (Y-13 C).
585
763
  const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, ordering.ops.filter(entry => entry.op === 'add_component').map(entry => entry.component));
@@ -587,17 +765,22 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
587
765
  environment,
588
766
  body: { ops: [...allOps.filter(entry => entry.op === 'resize_component').flatMap(resizeServerOps), ...ordering.ops] }
589
767
  })) ?? {});
768
+ const skeletonWarnings = await simulatedPageLayoutWarnings(platform, resolved, project, environment, applicationId, () => fetchPage(platform, project, environment, applicationId, pageId), opRecords, 'audit');
590
769
  if (validationIssues.length > 0)
591
770
  response.validationWarnings = validationIssues;
592
- if (ordering.warnings.length > 0)
593
- response.layoutWarnings = ordering.warnings;
771
+ if (ordering.warnings.length > 0 || profileWarnings.length > 0 || skeletonWarnings.length > 0)
772
+ response.layoutWarnings = [...ordering.warnings, ...profileWarnings, ...skeletonWarnings];
773
+ if (connectDataRejections.length > 0)
774
+ response.rejectedOps = connectDataRejections;
775
+ Object.assign(response, applicationTypeField(resolved));
594
776
  return withNormalization(response, [
595
777
  ...componentNameNote(normalized.names),
596
778
  ...componentDefaultsNote(normalized.components),
597
779
  ...validationNote(normalized.validations),
598
780
  ...catalogNotes,
599
781
  ...replacePropNote(normalized.props),
600
- ...flexIndexOrderNote(ordering.moved)
782
+ ...flexIndexOrderNote(ordering.moved),
783
+ ...mobileSpacingNote(spacing)
601
784
  ]);
602
785
  }));
603
786
  server.registerTool('page_schema_patch', {
@@ -660,13 +843,27 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
660
843
  const toggles = expandSectionToggles(opRecords, schema, taken);
661
844
  sectionNotes = boxSectionNote([...created, ...toggles.created], toggles.removed);
662
845
  }
663
- const normalized = normalizeSchemaOps(opRecords);
846
+ const normalized = normalizeSchemaOps(opRecords, await styleTargetTypes(opRecords, readPage));
664
847
  for (const entry of opRecords)
665
848
  reject('PAGE-016', entry, schemaOpValidationErrors([entry]));
666
849
  for (const entry of opRecords)
667
850
  reject('PAGE-032', entry, codePropTemplateErrors([entry]));
668
851
  for (const entry of opRecords)
669
852
  reject('PAGE-037', entry, [...addComponentIdErrors([entry]), ...(entry.op === 'add_component' ? missingBlocksNodes([entry.component]) : [])]);
853
+ const touchesConnection = opRecords.some(entry => entry.op === 'set_binding' || entry.op === 'set_prop_type' || entry.op === 'replace_prop' || entry.op === 'remove_connect_data');
854
+ let connectedProps = null;
855
+ if (touchesConnection) {
856
+ try {
857
+ connectedProps = connectedPropKeys((await readPage()).schema);
858
+ }
859
+ catch {
860
+ connectedProps = null;
861
+ }
862
+ }
863
+ for (const entry of opRecords) {
864
+ for (const issue of connectDataOpIssues(entry, connectedProps))
865
+ reject(issue.code, entry, [issue.message]);
866
+ }
670
867
  if (opRecords.some(entry => entry.op === 'set_event' || entry.op === 'add_event')) {
671
868
  const typesByElementId = new Map();
672
869
  try {
@@ -694,14 +891,21 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
694
891
  retargeted = retargetRootAdds(opRecords, PAGE_CONTAINER_ELEMENT_ID);
695
892
  }
696
893
  }
697
- const ordering = await orderByIndex(opRecords, readPage);
894
+ const resolved = await resolveProfile(platform, applicationId);
895
+ const spacing = await mobileSpacingForOps(resolved.profile.platform, opRecords, readPage);
896
+ const blocksMeaningful = resolved.profile.layout.blocksMeaningful;
897
+ // Mobil (dizi sırası): index platformun kendi add/move index'idir, konum çevirisi yapılmaz.
898
+ const ordering = resolved.profile.layout.orderBy === 'block-position'
899
+ ? await orderByIndex(opRecords, readPage)
900
+ : { ops: opRecords, moved: [], warnings: [] };
698
901
  const sentOps = ordering.ops;
699
902
  // Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
700
903
  // silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
701
904
  let deletedFlows = [];
702
905
  // remove_event opRecords'ta değil, ayrı listede durur; koşul yalnız opRecords'a baktığı sürece
703
906
  // yalnız olay kaldıran bir yama bu korumayı hiç tetiklemiyordu (2026-09-14).
704
- if ([...opRecords, ...removeEventOps].some(entry => ['remove_component', 'set_event', 'remove_event'].includes(String(entry.op)))) {
907
+ // `batch` içindeki silme de planı gerektirir; açma `orphanedFlowPlan`'da.
908
+ if ([...opRecords, ...removeEventOps].some(entry => ['remove_component', 'set_event', 'remove_event', 'batch'].includes(String(entry.op)))) {
705
909
  try {
706
910
  const page = await readPage();
707
911
  deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
@@ -719,7 +923,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
719
923
  const added = sentOps
720
924
  .filter(entry => entry.op === 'add_component')
721
925
  .map(entry => entry.component);
722
- assertScreenWriteGuards('page', { schema: added });
926
+ assertScreenWriteGuards('page', { schema: added }, resolved.profile.platform);
723
927
  const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, added);
724
928
  const formWarnings = [...formBindingWarnings(opRecords), ...formSubmitButtonWarnings(opRecords)];
725
929
  let flowWarnings = [];
@@ -757,10 +961,10 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
757
961
  // görünür, iç içeliği ancak hedef parent zinciri belli olunca anlarsın.
758
962
  nestedAfterOps = nestedRepeaterNodes(simulated);
759
963
  layoutWarnings = [
760
- ...pageContainerPositionWarning(page.schema),
761
- ...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
964
+ ...(blocksMeaningful ? pageContainerPositionWarning(page.schema) : []),
965
+ ...(blocksMeaningful ? verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched) : []),
762
966
  ...stringStyleWarning(stringStyleNodes(simulated)),
763
- ...inertFlexPropWarning(inertFlexPropNodes(simulated)),
967
+ ...(resolved.profile.layout.modes.has('grid') ? inertFlexPropWarning(inertFlexPropNodes(simulated)) : []),
764
968
  ...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'),
765
969
  ...spacingLayerWarnings(simulated, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')
766
970
  ];
@@ -770,22 +974,54 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
770
974
  }
771
975
  }
772
976
  if (nestedAfterOps.length > 0)
773
- throw new PlatformApiError(nestedRepeaterMessage(nestedAfterOps), 400);
977
+ throw new PlatformApiError(nestedRepeaterMessage(nestedAfterOps, resolved.profile.platform), 400);
774
978
  const removedEvents = planEventRemovals(removeEventOps);
775
979
  const resized = resizeOps.length > 0 ? planResizes((await readPage()).schema ?? [], resizeOps) : { serverOps: [], applied: [], warnings: [] };
776
980
  const propTypes = propTypeOps.length > 0 ? planPropTypes((await readPage()).schema ?? [], propTypeOps) : { serverOps: [], applied: [] };
777
981
  const serverOps = [...removeEventOps, ...resized.serverOps, ...propTypes.serverOps, ...sentOps];
778
- layoutWarnings = [...ordering.warnings, ...layoutWarnings];
982
+ const writesStyle = resolved.profile.platform !== 'web' && opRecords.some(entry => (entry.op === 'replace_prop' || entry.op === 'set_binding') && entry.prop === 'style');
983
+ const profileWarnings = await screenProfileWarnings(platform, resolved, project, environment, applicationId, {
984
+ kind: 'page',
985
+ ops: [...opRecords, ...propTypeOps],
986
+ savedNodes: writesStyle ? await readPage().then(page => page.schema, () => undefined) : undefined,
987
+ steps: await boundFlowSteps(platform, resolved, project, environment, 'page', applicationId, pageId, opRecords)
988
+ });
989
+ const skeletonWarnings = reshapesSkeleton(opRecords)
990
+ ? await simulatedPageLayoutWarnings(platform, resolved, project, environment, applicationId, readPage, sentOps, 'new')
991
+ : [];
992
+ layoutWarnings = [...ordering.warnings, ...layoutWarnings, ...profileWarnings, ...skeletonWarnings];
779
993
  // Tek istek, tek sürüm: öykünülen op'lar 2026-09-14'e kadar ayrı GET + tam PUT çiftleriydi ve
780
994
  // yamadan ÖNCE yazılıyordu — PATCH reddedilse bile kalıcıydılar. Artık hepsi aynı atomik PATCH'te.
781
995
  // baseVersion yalnız biliniyorsa gider (ajanın verdiği ya da bu çağrıda zaten okunan sayfanınki):
782
996
  // sürümü öğrenmek için ayrı GET atılmaz, platform satırı her yamada kilitler.
997
+ const sentVersion = baseVersion ?? (typeof cachedPage?.version === 'string' ? cachedPage.version : undefined);
783
998
  const response = ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, {
784
999
  environment,
785
- body: { ops: serverOps, baseVersion: baseVersion ?? (typeof cachedPage?.version === 'string' ? cachedPage.version : undefined) }
1000
+ body: { ops: serverOps, baseVersion: sentVersion }
1001
+ }).catch(async (error) => {
1002
+ throw await versionConflict(error, project, environment, applicationId, pageId, sentVersion, "page_schema_get ile yeniden okuyup planı tazeleyin ve yeni version'ı baseVersion olarak gönderin; aynı op'ları eski sürümle tekrar göndermek yine 409 alır.");
786
1003
  })) ?? {});
1004
+ const patched = (response.data && typeof response.data === 'object' && !Array.isArray(response.data) ? response.data : response);
1005
+ const unchanged = Array.isArray(patched.unchanged) ? patched.unchanged.map(String) : [];
1006
+ rememberVersion(project, environment, pageId, patched.newVersion);
1007
+ // g-05: platform (b-05) aynı yere aynı içerikle zaten duran add_component'i etkisiz sayar; bütün
1008
+ // op'lar etkisizse yazmaz ve eski baseVersion'a 409 demez. Ajan bunu "hata yok ama bir şey de
1009
+ // olmadı" diye okumasın: yanıtı kaybolan çağrının tekrarı olduğu açıkça söylenir.
1010
+ if (unchanged.length > 0) {
1011
+ response.alreadyApplied = patched.noop !== true
1012
+ ? `Şu op'lar sayfada zaten duruyordu, yeniden yazılmadı (tekrar zararsız): ${unchanged.join(', ')}. Diğer op'lar uygulandı.`
1013
+ : patched.stale === true
1014
+ ? `Bu yamanın bütün op'ları sayfada zaten duruyor (${unchanged.join(', ')}) — hiçbir şey yazılmadı. AMA sayfa gönderdiğiniz baseVersion'dan beri başka bir yazımla da değişti: newVersion sizin gönderdiğiniz sürümdür, güncel sürüm değil. Devam etmeden önce page_schema_get ile güncel hâli okuyun; eski sürümle gelen sonraki yazma 409 alır.`
1015
+ : `Bu yamanın bütün op'ları sayfada zaten duruyor (${unchanged.join(', ')}): büyük olasılıkla önceki çağrı uygulandı ama yanıtı size ulaşmadı. Tekrar zararsızdır — hiçbir şey yazılmadı, sürüm artmadı; newVersion sayfanın güncel sürümüdür.`;
1016
+ }
787
1017
  if (view !== 'full')
788
1018
  outlineWriteResponse(response);
1019
+ if (baseVersion && schemaReadMemory.wasTruncated(readKey(project, environment, pageId, baseVersion))) {
1020
+ response.warnings = [
1021
+ ...(Array.isArray(response.warnings) ? response.warnings : []),
1022
+ `PAGE-038 (uyarı) — baseVersion '${baseVersion}' bu gateway'de page_schema_get allowTruncated:true ile KIRPIK okundu; plan düşen düğümleri görmeden yapılmış olabilir. Micro-op veri düşürmez ve yama uygulandı, ama hedef ve konumları page_schema_get mode:'summary' ya da cursor:'0' ile tam okuyup doğrulayın.`
1023
+ ];
1024
+ }
789
1025
  if (removedEvents.length > 0)
790
1026
  response.removedEvents = removedEvents;
791
1027
  if (propTypes.applied.length > 0)
@@ -829,6 +1065,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
829
1065
  opDrops = emptyWriteDiff();
830
1066
  }
831
1067
  }
1068
+ Object.assign(response, applicationTypeField(resolved));
832
1069
  return withWriteDiff(withNormalization(response, [
833
1070
  ...rootAddsNote(retargeted, PAGE_CONTAINER_ELEMENT_ID),
834
1071
  ...componentNameNote(normalized.names),
@@ -837,7 +1074,8 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
837
1074
  ...validationNote(normalized.validations),
838
1075
  ...catalogNotes,
839
1076
  ...replacePropNote(normalized.props),
840
- ...flexIndexOrderNote(ordering.moved)
1077
+ ...flexIndexOrderNote(ordering.moved),
1078
+ ...mobileSpacingNote(spacing)
841
1079
  ]), opDrops);
842
1080
  }));
843
1081
  server.registerTool('page_discover', {
@@ -868,24 +1106,39 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
868
1106
  layoutId: guid.optional(),
869
1107
  roleIds: z.array(guid).optional().describe('Sayfayı görebilecek roller; boş = herkese açık'),
870
1108
  middlewareIds: z.array(guid).optional(),
1109
+ isAnonymous: z.boolean().optional().describe('Anonim uç: true ise sayfa kompleks projede oturumsuz da açılır (erişim kapısı atlanır). Giriş ekranı ve genel açılış sayfaları için; veri gösteren sayfalarda açmayın.'),
1110
+ isLoginPage: z.boolean().optional().describe("Uygulamanın giriş sayfası. Bir application'da yalnız bir sayfa bu bayrağı taşımalıdır; sayfa reddi (403) bu sayfaya yönlendirilir. Giriş sayfası oturumsuz açılabilmelidir, bu yüzden isAnonymous ile birlikte kullanın."),
871
1111
  events: eventsParam.optional(),
872
1112
  schema: schemaParam.optional(),
873
1113
  view: writeViewParam,
874
- ifExists: ifExistsParam('sayfa', "mevcut sayfayı page_update gibi günceller — yalnız bu çağrıda VERİLEN alanlar yazılır, verilmeyen şema/olaylar/roller korunur (page_schema_patch ile kurulmuş şema betik tekrar koşunca silinmez); id'si ve flow'ları korunur")
1114
+ ifExists: ifExistsParam('sayfa', "mevcut sayfayı page_update gibi günceller — yalnız bu çağrıda VERİLEN alanlar yazılır, verilmeyen şema/olaylar/roller korunur (page_schema_patch ile kurulmuş şema betik tekrar koşunca silinmez); id'si ve flow'ları korunur. Şema verilirse sayfanın şeması BİLEREK tam yazılır (page_update force:true gibi — gateway sayfayı okuduğu anın sürümüyle yazar, aradaki Studio değişikliği ezilir); hedefli değişiklik için page_schema_patch")
875
1115
  },
876
1116
  annotations: writes
877
- }, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, events, schema, view, ifExists }) => {
878
- const container = ensurePageContainer(schema ?? []);
1117
+ }, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, isAnonymous, isLoginPage, events, schema, view, ifExists }) => {
1118
+ const resolved = await resolveProfile(platform, applicationId);
1119
+ const blocksMeaningful = resolved.profile.layout.blocksMeaningful;
1120
+ const container = ensurePageContainer(schema ?? [], resolved.profile.layout.rootContainer);
1121
+ const spacing = emptyMobileSpacingFix();
1122
+ // Kabuksuz iskelet yalnız gateway'in kurduğu köke uygulanır; sayfanın layout'u yazımda kesinleşir.
1123
+ const templateRoot = resolved.profile.platform === 'mobile' && container.fixes.length > 0 ? container.schema[0] : undefined;
1124
+ let skeletonNotes = [];
879
1125
  const named = fillComponentNames(container.schema);
880
- assertScreenWriteGuards('page', { schema: container.schema, events });
1126
+ if (resolved.profile.platform === 'mobile') {
1127
+ for (const root of container.schema)
1128
+ applyMobileSpacingDefaults(root.children, [], spacing, String(root.elementId ?? ''));
1129
+ }
1130
+ assertScreenWriteGuards('page', { schema: container.schema, events }, resolved.profile.platform);
881
1131
  const stringStyles = stringStyleNodes(container.schema);
882
1132
  const fixed = normalizeComponentDefaults(container.schema);
883
- const reset = resetPageContainerPosition(container.schema);
1133
+ // Mobil çizer blokları okumaz (D3): kök konumu sıfırlama, blok sıralama ve dikey boşluk
1134
+ // uyarısı yalnız ızgaranın anlamlı olduğu profilde koşar.
1135
+ const reset = blocksMeaningful ? resetPageContainerPosition(container.schema) : [];
884
1136
  const catalogNotes = (schema ?? []).length > 0
885
- ? await screenCatalogNotes(platform, project, environment, applicationId, container.schema)
1137
+ ? await assertScreenCatalog(platform, project, environment, applicationId, container.schema, 'page_create')
886
1138
  : [];
887
- const moved = reorderSchemaBlocks(container.schema);
888
- const gaps = (schema ?? []).length > 0 ? verticalGapWarnings(container.schema, 'PAGE-010 (uyarı)') : [];
1139
+ const moved = blocksMeaningful ? reorderSchemaBlocks(container.schema) : [];
1140
+ const gaps = (schema ?? []).length > 0 && blocksMeaningful ? verticalGapWarnings(container.schema, 'PAGE-010 (uyarı)') : [];
1141
+ const profileWarnings = await screenProfileWarnings(platform, resolved, project, environment, applicationId, { kind: 'page', nodes: container.schema });
889
1142
  const existing = await existingMatch({
890
1143
  code: 'PAGE-035',
891
1144
  noun: 'sayfa',
@@ -902,28 +1155,36 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
902
1155
  if (existing !== undefined) {
903
1156
  const pageId = String(existing.id);
904
1157
  const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
1158
+ if (templateRoot && schema !== undefined && (layoutId ?? current.layoutId ?? current.layout?.id ?? null) === null) {
1159
+ skeletonNotes = shellLessPageSkeleton(templateRoot);
1160
+ }
905
1161
  const overrides = Object.fromEntries(Object.entries({
906
- name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, events,
1162
+ name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, isAnonymous, isLoginPage, events,
907
1163
  schema: schema === undefined ? undefined : container.schema
908
1164
  }).filter(([, value]) => value !== undefined));
909
1165
  const body = pageUpdateBody(current, overrides);
910
1166
  const before = pageUpdateBody(current);
911
1167
  // Aynı ad/slug ile tekrar koşan betik o alanları "değişti" diye görmemeli (Gözlem 2).
912
1168
  const changed = Object.keys(overrides).filter(key => JSON.stringify(before[key]) !== JSON.stringify(overrides[key]));
913
- await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
1169
+ const written = await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
1170
+ rememberVersion(project, environment, pageId, written?.data?.version);
914
1171
  const updateWarnings = schema === undefined
915
1172
  ? []
916
- : [...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ı)')];
1173
+ : [...gaps, ...stringStyleWarning(stringStyles), ...(resolved.profile.layout.modes.has('grid') ? inertFlexPropWarning(inertFlexPropNodes(container.schema)) : []), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'), ...spacingLayerWarnings(container.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)'), ...profileWarnings,
1174
+ ...await pageLayoutWarnings(platform, resolved, project, environment, applicationId, { tree: container.schema, layoutId: layoutId ?? current.layoutId ?? current.layout?.id })];
917
1175
  const summary = {
918
1176
  id: pageId,
919
1177
  outcome: 'updated',
920
1178
  changed,
921
- preserved: { schemaNodes: countSchemaNodes(body.schema), events: body.events.length }
1179
+ preserved: { schemaNodes: countSchemaNodes(body.schema), events: body.events.length },
1180
+ ...applicationTypeField(resolved)
922
1181
  };
923
1182
  if (updateWarnings.length > 0)
924
1183
  summary.layoutWarnings = updateWarnings;
925
- return withNormalization(summary, schema === undefined ? [] : [...container.fixes, ...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]);
1184
+ return withNormalization(summary, schema === undefined ? [] : [...container.fixes, ...skeletonNotes, ...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved), ...mobileSpacingNote(spacing)]);
926
1185
  }
1186
+ if (templateRoot && layoutId === undefined)
1187
+ skeletonNotes = shellLessPageSkeleton(templateRoot);
927
1188
  const response = await platform.tenant('POST', project, `/pages/${applicationId}`, {
928
1189
  environment,
929
1190
  body: {
@@ -934,6 +1195,8 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
934
1195
  layoutId: layoutId ?? null,
935
1196
  roleIds: roleIds ?? [],
936
1197
  middlewareIds: middlewareIds ?? [],
1198
+ isAnonymous: isAnonymous ?? false,
1199
+ isLoginPage: isLoginPage ?? false,
937
1200
  events: events ?? [],
938
1201
  schema: container.schema
939
1202
  }
@@ -941,6 +1204,8 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
941
1204
  const saved = (response && typeof response === 'object' && !Array.isArray(response)
942
1205
  ? (response.data ?? response)
943
1206
  : {});
1207
+ if (typeof saved.id === 'string')
1208
+ rememberVersion(project, environment, saved.id, saved.version);
944
1209
  // Kırpmadan ÖNCE doğrulama yolu (B-55): yanıt gövdesi iskelete inince "gönderdiğim prop
945
1210
  // kaydedildi mi" sorusunu gövde değil bu diff cevaplar. `layout_create`'in yaptığının sayfa
946
1211
  // ikizi; Prop sarmalı açılmazsa doğru kaydedilmiş her prop düşmüş görünür (G-28).
@@ -950,15 +1215,20 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
950
1215
  if (view !== 'full' && Array.isArray(saved.schema)) {
951
1216
  saved.schema = saved.schema.map(node => summarize(node));
952
1217
  }
953
- const normalized = withWriteDiff(withNormalization(withOutcome(response, undefined), [...container.fixes, ...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), drops);
954
- 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ı)')];
955
- if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
956
- return normalized;
957
- return { ...normalized, layoutWarnings: createWarnings };
1218
+ const normalized = withWriteDiff(withNormalization(withOutcome(response, undefined), [...container.fixes, ...skeletonNotes, ...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved), ...mobileSpacingNote(spacing)]), drops);
1219
+ const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...(resolved.profile.layout.modes.has('grid') ? inertFlexPropWarning(inertFlexPropNodes(container.schema)) : []), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'), ...spacingLayerWarnings(container.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)'), ...profileWarnings,
1220
+ ...await pageLayoutWarnings(platform, resolved, project, environment, applicationId, { tree: container.schema, layoutId })];
1221
+ const typeField = applicationTypeField(resolved);
1222
+ const findings = await legacyRoleListFindings(platform, project, environment, { roleIds }, 'page');
1223
+ if (!normalized || typeof normalized !== 'object')
1224
+ return attachFindings(normalized, findings);
1225
+ if (createWarnings.length === 0)
1226
+ return attachFindings(Object.keys(typeField).length === 0 ? normalized : { ...normalized, ...typeField }, findings);
1227
+ return attachFindings({ ...normalized, ...typeField, layoutWarnings: createWarnings }, findings);
958
1228
  }));
959
1229
  server.registerTool('page_update', {
960
1230
  title: 'Sayfa üstbilgisini güncelle',
961
- description: "Sayfanın adını/slug'ını/layout'unu/rollerini değiştirir. Platformun PUT'u REPLACE'tir; gateway sayfayı önce okur, yalnız verdiğiniz alanları değiştirip tam gövdeyi geri yazar — bu yüzden şema ve olaylar kaybolmaz. ŞEMA değişikliği için bu tool'u değil page_schema_patch'i kullanın (doğrulama duvarı + canlı canvas orada).",
1231
+ description: "Sayfanın adını/slug'ını/layout'unu/rollerini değiştirir. Platformun PUT'u REPLACE'tir; gateway sayfayı önce okur, yalnız verdiğiniz alanları değiştirip tam gövdeyi geri yazar — bu yüzden şema ve olaylar kaybolmaz. ŞEMA değişikliği için bu tool'u değil page_schema_patch'i kullanın (doğrulama duvarı + canlı canvas orada). SÜRÜMSÜZ YAZMAZ (PAGE-039): baseVersion verilmezse bu gateway'in o sayfada en son gördüğü sürüm gider (page_get, page_schema_get, page_schema_patch'in newVersion'ı ya da önceki page_update); hiçbiri yoksa istek gönderilmez — önce okuyun. Sayfa o sürümden beri değiştiyse (Studio'da açık sekme, başka ajan) 409 döner, kim ve ne zaman değiştirdiği söylenir ve hiçbir şey yazılmaz. force:true son sürümü okuyup onun üstüne bilerek yazar (yanıt forced: true).",
962
1232
  inputSchema: {
963
1233
  project: projectParam,
964
1234
  environment: environmentParam,
@@ -971,32 +1241,75 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
971
1241
  layoutId: guid.nullable().optional().describe('null: layout bağını kaldırır'),
972
1242
  roleIds: z.array(guid).optional(),
973
1243
  middlewareIds: z.array(guid).optional(),
1244
+ isAnonymous: z.boolean().optional().describe('Anonim uç: true ise sayfa kompleks projede oturumsuz da açılır (erişim kapısı atlanır). Giriş ekranı ve genel açılış sayfaları için; veri gösteren sayfalarda açmayın.'),
1245
+ isLoginPage: z.boolean().optional().describe("Uygulamanın giriş sayfası. Bir application'da yalnız bir sayfa bu bayrağı taşımalıdır; sayfa reddi (403) bu sayfaya yönlendirilir. Giriş sayfası oturumsuz açılabilmelidir, bu yüzden isAnonymous ile birlikte kullanın."),
974
1246
  events: eventsParam.optional(),
975
- schema: schemaParam.optional().describe('Nadiren gerekir; verilirse her düğümde elementId, name (= elementId), type, blocks zorunlu (PAGE-037, gateway blocks doldurmaz); verilmezse mevcut şema korunur')
1247
+ schema: schemaParam.optional().describe('Nadiren gerekir; verilirse her düğümde elementId, name (= elementId), type, blocks zorunlu (PAGE-037, gateway blocks doldurmaz); verilmezse mevcut şema korunur. Tam REPLACE: kayıtlı sayfadan daha az düğüm taşıyan şema allowNodeLoss olmadan reddedilir (PAGE-043)'),
1248
+ allowNodeLoss: z.boolean().optional().describe('true: gönderilen schema kayıtlı sayfadan daha az düğüm taşısa da yazılır — eksik düğümler geri dönüşsüz silinir. Varsayılan false: PAGE-043 ile reddedilir (kırpık okumanın geri yazılması)'),
1249
+ baseVersion: z.string().optional().describe("Değişikliği planladığınız sayfa sürümü (page_get / page_schema_get yanıtındaki version). Verilmezse bu gateway'in o sayfada en son gördüğü sürüm gider; sayfa o sürümden beri değiştiyse 409 döner ve hiçbir şey yazılmaz"),
1250
+ force: z.boolean().optional().describe("true: sürüm kontrolünü bilerek geçer — gateway sayfanın son sürümünü okuyup onun üstüne yazar, aradaki değişiklik (Studio sekmesi, başka ajan) ezilir. Yanıt forced: true taşır. Varsayılan false")
976
1251
  },
977
1252
  annotations: updates
978
- }, handle(config, async ({ project, environment, applicationId, pageId, ...changes }) => {
1253
+ }, handle(config, async ({ project, environment, applicationId, pageId, allowNodeLoss, baseVersion, force, ...changes }) => {
1254
+ const knownVersion = baseVersion ?? schemaReadMemory.lastVersion(pageKey(project, environment, pageId));
1255
+ if (knownVersion === undefined && force !== true) {
1256
+ throw new PlatformApiError("PAGE-039 — page_update hangi sürümün üstüne yazdığını bilmiyor: baseVersion verilmedi ve bu gateway sayfayı henüz okumadı. Platformun PUT'u REPLACE'tir; sürümsüz tam gövde Studio'da açık bir sekmenin ya da başka bir ajanın değişikliğini sessizce ezer (Bitki Bakım koşusu, 14:02). Önce page_get ya da page_schema_get ile okuyun — yanıttaki version gateway'de hatırlanır — ya da onu baseVersion olarak verin. Son sürümün üstüne bilerek yazmak için force:true. İstek gönderilmedi.", 400);
1257
+ }
979
1258
  const overrides = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== undefined));
980
1259
  const named = fillComponentNames(overrides.schema);
981
- assertScreenWriteGuards('page', { schema: overrides.schema, events: overrides.events });
1260
+ const resolved = overrides.schema === undefined ? undefined : await resolveProfile(platform, applicationId);
1261
+ assertScreenWriteGuards('page', { schema: overrides.schema, events: overrides.events }, resolved?.profile.platform);
982
1262
  const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
1263
+ if (overrides.schema !== undefined && allowNodeLoss !== true) {
1264
+ const storedSchema = current.schema;
1265
+ const sentSchema = overrides.schema;
1266
+ const stored = countSchemaNodes(storedSchema);
1267
+ const sent = countSchemaNodes(sentSchema);
1268
+ if (sent < stored) {
1269
+ const missing = missingElementIds(storedSchema, sentSchema);
1270
+ const listed = missing.slice(0, 10).join(', ');
1271
+ const more = missing.length > 10 ? ` (+${missing.length - 10} daha)` : '';
1272
+ throw new PlatformApiError(`PAGE-043 — gönderilen şema ${sent} düğüm, kayıtlı sayfa ${stored} düğüm; eksilen elementId'ler: ${listed}${more}. Platformun PUT'u REPLACE'tir: eksik düğümler geri dönüşsüz silinir — kırpık (allowTruncated) ya da yarım bir okuma geri yazılıyor olabilir, istek gönderilmedi. Şema değişikliğini page_schema_patch micro-op'larıyla yapın (silmek için remove_component); düğümleri bilerek siliyorsanız allowNodeLoss:true verin.`, 400);
1273
+ }
1274
+ }
1275
+ const blocksMeaningful = resolved?.profile.layout.blocksMeaningful ?? true;
983
1276
  const fixed = normalizeComponentDefaults(overrides.schema);
984
- const reset = overrides.schema === undefined ? [] : resetPageContainerPosition(overrides.schema);
985
- const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, overrides.schema);
986
- const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
1277
+ const reset = overrides.schema === undefined || !blocksMeaningful ? [] : resetPageContainerPosition(overrides.schema);
1278
+ const catalogNotes = await assertScreenCatalog(platform, project, environment, applicationId, overrides.schema, 'page_update');
1279
+ const moved = overrides.schema === undefined || !blocksMeaningful ? [] : reorderSchemaBlocks(overrides.schema);
987
1280
  const gaps = overrides.schema === undefined
988
1281
  ? []
989
- : [...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ı)')];
1282
+ : [
1283
+ ...(blocksMeaningful ? verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)') : []),
1284
+ ...(resolved?.profile.layout.modes.has('grid') ?? true ? inertFlexPropWarning(inertFlexPropNodes(overrides.schema)) : []),
1285
+ ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'),
1286
+ ...spacingLayerWarnings(overrides.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)'),
1287
+ ...(resolved ? await screenProfileWarnings(platform, resolved, project, environment, applicationId, { kind: 'page', nodes: overrides.schema }) : [])
1288
+ ];
990
1289
  const body = pageUpdateBody(current, overrides);
991
- await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
1290
+ // Mizanpaj iskeleti (MOB-015…018) kabukla birlikte karar verilir: sayfa kabuğa bağlanınca ya da kabuktan
1291
+ // çıkınca da denetlenir (kabuksuz iskeletteki üst pay, payı veren kabukta çift olur).
1292
+ const skeletonProfile = resolved ?? (overrides.layoutId !== undefined ? await resolveProfile(platform, applicationId) : undefined);
1293
+ const skeletonWarnings = skeletonProfile
1294
+ ? await pageLayoutWarnings(platform, skeletonProfile, project, environment, applicationId, { tree: body.schema, layoutId: body.layoutId })
1295
+ : [];
1296
+ if (force !== true)
1297
+ body.baseVersion = knownVersion;
1298
+ const written = await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body }).catch(async (error) => {
1299
+ throw await versionConflict(error, project, environment, applicationId, pageId, String(body.baseVersion ?? ''), "page_schema_get ile yeniden okuyun ve şema değişikliğini page_schema_patch ile uygulayın — yalnız dokunduğunuz yer yazılır, aradaki değişiklik korunur. Üstbilgi (ad, slug, layout, roller) için okuduktan sonra page_update'i yeniden çağırın; aradaki değişikliği bilerek ezmek için force:true.");
1300
+ });
1301
+ rememberVersion(project, environment, pageId, written?.data?.version);
992
1302
  const summary = {
993
1303
  id: pageId,
994
1304
  changed: Object.keys(overrides),
995
- preserved: { schemaNodes: body.schema.length, events: body.events.length }
1305
+ preserved: { schemaNodes: countSchemaNodes(body.schema), events: body.events.length }
996
1306
  };
997
- if (gaps.length > 0)
998
- summary.layoutWarnings = gaps;
999
- return withNormalization(summary, [...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]);
1307
+ if (force === true)
1308
+ summary.forced = true;
1309
+ if (gaps.length > 0 || skeletonWarnings.length > 0)
1310
+ summary.layoutWarnings = [...gaps, ...skeletonWarnings];
1311
+ const findings = await legacyRoleListFindings(platform, project, environment, overrides, 'page');
1312
+ return attachFindings(withNormalization(summary, [...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), findings);
1000
1313
  }));
1001
1314
  server.registerTool('page_clone', {
1002
1315
  title: 'Sayfa klonla',
@@ -1004,6 +1317,26 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
1004
1317
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
1005
1318
  annotations: writes
1006
1319
  }, handle(config, ({ project, environment, applicationId, pageId }) => platform.tenant('POST', project, `/pages/${applicationId}/clone/${pageId}`, { environment })));
1320
+ server.registerTool('page_restore', {
1321
+ title: 'Sayfayı bir revizyona döndür',
1322
+ description: "Sayfayı revizyon defterindeki N. sürümüne döndürür: şema (komponentler eski id'leriyle), sayfa olayları, roller, middleware'ler ve layout o sürümdeki gibi yazılır; o sürümün bağladığı ama bugün silinmiş akışlar da kendi son revizyonlarından AYNI id ile geri kurulur (önce adımsız akış, sonra şema, sonra adımlar — tek transaction). Boşalan ya da bayat bir kaydın ezdiği sayfayı (ZM-27) böyle geri alın; tek akış için flow_restore. N'yi revision_list(module: 'page', recordId: <pageId>) ile bulun, içeriğini revision_get/revision_diff ile doğrulayın. N'den SONRA bağlanan akışlar yetim kalır ve silinir — yanıt deletedFlows ile listeler, flow_restore ile geri gelir. baseVersion (page_schema_get version) verilirse sayfa o sürümden beri değiştiyse 409. N'nin bağladığı bir akışın revizyonu kalmamışsa 409 ve hiçbir şey yazılmaz (hata akış id'lerini söyler). DİKKAT: şemayı yüklemeyen komutların (UpdateProjectCommand, UpdateApplicationCommand…) yazdığı sayfa revizyonları komponent TAŞIMAZ; komponenti olan bir sayfayı böyle bir revizyona döndürmek platformca 409 ile reddedilir — correlationId'si sayfa kaydı olan (PatchPageSchemaCommand, UpdatePageCommand) revizyonu seçin. Sayfayı bilerek boşaltmak için allowEmpty: true.",
1323
+ inputSchema: {
1324
+ project: projectParam,
1325
+ environment: environmentParam,
1326
+ applicationId: guid,
1327
+ pageId: guid,
1328
+ revisionNo: z.number().int().min(0).describe("Dönülecek sayfa revizyonunun numarası (revision_list revisionNo)"),
1329
+ baseVersion: z.string().min(1).optional().describe('page_schema_get çıktısındaki version; sayfa o sürümden beri değiştiyse 409 döner ve hiçbir şey yazılmaz'),
1330
+ allowEmpty: z.boolean().optional().describe('true: komponentsiz bir revizyona dönüp sayfayı bilerek boşalt. Varsayılan false — platform komponenti olan sayfayı boş revizyona döndürmeyi 409 ile reddeder')
1331
+ },
1332
+ annotations: destructive
1333
+ }, handle(config, async ({ project, environment, applicationId, pageId, revisionNo, baseVersion, allowEmpty }) => {
1334
+ const restored = unwrap(await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/restore`, {
1335
+ environment,
1336
+ body: { revisionNo, ...(baseVersion ? { baseVersion } : {}), ...(allowEmpty ? { allowEmpty: true } : {}) }
1337
+ }));
1338
+ return { pageId, ...restored };
1339
+ }));
1007
1340
  server.registerTool('page_delete', {
1008
1341
  title: 'Sayfa sil',
1009
1342
  description: 'Sayfayı siler. Ana sayfa (isHomepage) silinemez; önce başka bir sayfayı ana sayfa yapın. Silmeden önce dependency_graph module:\'page\' ile bağımlılıkları kontrol edin.',
@@ -1018,3 +1351,37 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
1018
1351
  return { deleted: pageId, name: page.name };
1019
1352
  }));
1020
1353
  }
1354
+ /**
1355
+ * Mobil yazımda yeni düğümlerin boşluk varsayılanı (mizanpaj S5/S6); kardeşler kayıtlı ağaçtan okunur.
1356
+ * Ağaç okunamazsa dokunulmaz: eski bir kaba eklenen düğümü yanlışlıkla kaydırmaktansa varsayılan kalsın.
1357
+ */
1358
+ async function mobileSpacingForOps(platformName, ops, readSaved) {
1359
+ const fix = emptyMobileSpacingFix();
1360
+ if (platformName !== 'mobile' || !ops.some(entry => entry.op === 'add_component' || entry.op === 'batch'))
1361
+ return fix;
1362
+ const saved = await readSaved().then(page => page.schema, () => undefined);
1363
+ if (saved !== undefined)
1364
+ applyMobileSpacingToOps(ops, saved, fix);
1365
+ return fix;
1366
+ }
1367
+ /**
1368
+ * Yazımdan sonraki sayfa ağacının mizanpaj iskeleti (MOB-015…018). `audit` (kuru koşu) ağacın tamamını,
1369
+ * `new` (yama) yalnız yazımın getirdiği bulguları söyler — yoksa bozuk bir sayfaya yapılan her yama aynı
1370
+ * uyarıları tekrarlardı. Kayıtlı sayfa okunamazsa denetim koşmaz.
1371
+ */
1372
+ async function simulatedPageLayoutWarnings(platform, resolved, project, environment, applicationId, readSaved, ops, mode) {
1373
+ if (resolved.profile.platform !== 'mobile')
1374
+ return [];
1375
+ const page = await readSaved().catch(() => undefined);
1376
+ if (!page)
1377
+ return [];
1378
+ const saved = page.schema ?? [];
1379
+ const simulated = JSON.parse(JSON.stringify(saved));
1380
+ simulateLayoutOps(simulated, ops);
1381
+ applySkeletonOps(simulated, ops);
1382
+ return pageLayoutWarnings(platform, resolved, project, environment, applicationId, {
1383
+ tree: simulated,
1384
+ ...(mode === 'new' ? { before: saved } : {}),
1385
+ layoutId: page.layoutId ?? page.layout?.id
1386
+ });
1387
+ }