@appaflytech/zeyrek-ai-mcp 0.0.4 → 0.0.6

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 (101) hide show
  1. package/.env.example +8 -3
  2. package/.env.production.example +2 -0
  3. package/README.md +229 -193
  4. package/dist/config.js +20 -2
  5. package/dist/knowledge/backendConstraints.js +897 -0
  6. package/dist/knowledge/componentCatalog.js +196 -42
  7. package/dist/knowledge/coverage.js +43 -0
  8. package/dist/knowledge/flowGuards.js +30 -0
  9. package/dist/knowledge/guideSections.js +49 -0
  10. package/dist/knowledge/hydrate.js +97 -0
  11. package/dist/knowledge/languageCodes.js +36 -0
  12. package/dist/knowledge/languages.js +105 -0
  13. package/dist/knowledge/layoutSummary.js +10 -0
  14. package/dist/knowledge/localization.js +51 -0
  15. package/dist/knowledge/normalize.js +449 -16
  16. package/dist/knowledge/outlines.js +136 -12
  17. package/dist/knowledge/propTypes.js +80 -0
  18. package/dist/knowledge/rules.js +618 -43
  19. package/dist/knowledge/schemaGuard.js +199 -0
  20. package/dist/knowledge/styleResolve.js +63 -4
  21. package/dist/observability/trace.js +27 -3
  22. package/dist/platform/errors.js +46 -4
  23. package/dist/platform/http.js +33 -4
  24. package/dist/platform/patch.js +27 -0
  25. package/dist/platform/refs.js +104 -0
  26. package/dist/platform/responses.js +9 -2
  27. package/dist/platform/screenshot.js +527 -16
  28. package/dist/platform/secrets.js +40 -0
  29. package/dist/platform/tokenVault.js +89 -0
  30. package/dist/platform/writeDiff.js +281 -0
  31. package/dist/resources/examples/layout-app-shell.json +1 -1
  32. package/dist/resources/examples/microservice-table-list.json +2 -4
  33. package/dist/resources/examples/page-dynamic-form.json +2 -2
  34. package/dist/resources/examples/page-flex-sections.json +99 -0
  35. package/dist/resources/examples/page-sign-in.json +3 -2
  36. package/dist/resources/guides/component.md +69 -8
  37. package/dist/resources/guides/entity.md +404 -16
  38. package/dist/resources/guides/expressions.md +44 -2
  39. package/dist/resources/guides/external-component.md +9 -0
  40. package/dist/resources/guides/getting-started.md +77 -7
  41. package/dist/resources/guides/layout.md +53 -10
  42. package/dist/resources/guides/microservice.md +226 -13
  43. package/dist/resources/guides/middleware.md +89 -1
  44. package/dist/resources/guides/page.md +383 -46
  45. package/dist/resources/guides/translation.md +126 -8
  46. package/dist/resources/guides/view.md +16 -1
  47. package/dist/resources/guides/workflow.md +198 -27
  48. package/dist/resources/knowledge.js +5 -0
  49. package/dist/resources/rules.js +27 -0
  50. package/dist/resources/schema-fingerprint.json +8 -0
  51. package/dist/resources/schemas/CreateBulkRowCommand.json +33 -0
  52. package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
  53. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
  54. package/dist/resources/schemas/CreateEntityCommand.json +52 -12
  55. package/dist/resources/schemas/CreateMicroserviceCommand.json +26 -2
  56. package/dist/resources/schemas/CreateQueryCommand.json +56 -3
  57. package/dist/resources/schemas/CreateRowCommand.json +38 -0
  58. package/dist/resources/schemas/CreateViewCommand.json +13 -10
  59. package/dist/resources/schemas/CreateWorkflowCommand.json +95 -14
  60. package/dist/resources/schemas/GetRowsQuery.json +3 -0
  61. package/dist/resources/schemas/PreviewMicroserviceCommand.json +16 -0
  62. package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
  63. package/dist/resources/schemas/PreviewWorkflowCommand.json +34 -9
  64. package/dist/resources/schemas/StepDto.json +23 -3
  65. package/dist/resources/schemas/UpdateBulkRowCommand.json +33 -0
  66. package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
  67. package/dist/resources/schemas/UpdateEntityCommand.json +49 -10
  68. package/dist/resources/schemas/UpdateMicroserviceCommand.json +28 -3
  69. package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
  70. package/dist/resources/schemas/UpdateRowCommand.json +40 -0
  71. package/dist/resources/schemas/UpdateViewCommand.json +15 -10
  72. package/dist/resources/schemas/UpdateWorkflowCommand.json +95 -14
  73. package/dist/server.js +4 -0
  74. package/dist/tools/access.js +3 -3
  75. package/dist/tools/applications.js +1 -1
  76. package/dist/tools/automations.js +63 -30
  77. package/dist/tools/common.js +39 -0
  78. package/dist/tools/components.js +16 -7
  79. package/dist/tools/datasources.js +83 -21
  80. package/dist/tools/deployments.js +34 -2
  81. package/dist/tools/dtos.js +25 -12
  82. package/dist/tools/entities.js +113 -28
  83. package/dist/tools/externalComponents.js +9 -3
  84. package/dist/tools/files.js +20 -6
  85. package/dist/tools/flows.js +110 -11
  86. package/dist/tools/folders.js +7 -2
  87. package/dist/tools/globalFunctions.js +63 -0
  88. package/dist/tools/layouts.js +103 -29
  89. package/dist/tools/microservices.js +229 -38
  90. package/dist/tools/organizations.js +5 -6
  91. package/dist/tools/pages.js +365 -100
  92. package/dist/tools/platformInfo.js +152 -11
  93. package/dist/tools/projects.js +183 -33
  94. package/dist/tools/queries.js +32 -10
  95. package/dist/tools/rows.js +159 -45
  96. package/dist/tools/screenWrite.js +59 -0
  97. package/dist/tools/screens.js +28 -0
  98. package/dist/tools/translations.js +24 -8
  99. package/dist/tools/views.js +20 -10
  100. package/dist/tools/workflows.js +33 -16
  101. package/package.json +6 -3
@@ -1,15 +1,36 @@
1
- import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
2
- import { orphanedFlowWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, blockSizeNote, heightTypeNote, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, invalidScreenEventNames, invalidWidgetTypeNodes, withNormalization, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
3
- import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode } from './screens.js';
4
- import { applyCatalogLayoutRules, fetchComponentEventNames } from './components.js';
5
- import { formBindingWarnings } from '../knowledge/coverage.js';
1
+ import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive, microOps } from './common.js';
2
+ import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, staleSetterWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, isFlexContainer, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, withNormalization, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
3
+ import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, outlineAppliedPatch, findNode, flowPath } from './screens.js';
4
+ import { fetchComponentEventNames } from './components.js';
5
+ import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
6
+ import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
7
+ import { schemaOpDrops, silentlyDropped, unwrapProps, withWriteDiff } from '../platform/writeDiff.js';
6
8
  import { PlatformApiError } from '../platform/errors.js';
7
9
  import { handle, toolResult, toolError } from '../platform/responses.js';
8
10
  import { StudioScreenshotter } from '../platform/screenshot.js';
9
- import { writeFile } from 'node:fs/promises';
10
- import { resolve } from 'node:path';
11
+ import { summarizeLayout } from '../knowledge/layoutSummary.js';
12
+ import { resolveIdentityToken } from '../platform/tokenVault.js';
13
+ import { mkdir, writeFile } from 'node:fs/promises';
14
+ import { dirname, resolve } from 'node:path';
15
+ import { applyPropTypesToSchema, setPropTypeErrors } from '../knowledge/propTypes.js';
11
16
  import { z } from 'zod';
12
- const opsParam = z.array(z.record(z.unknown())).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} | set_binding {target, prop, expression: '{{ Page.variables.x }}'} | add_component {parent (üst elementId ya da 'root'), index?, component} | remove_component {target} | move_component {target, parent, index?, position: {x, y}} | set_event {target, event, eventValue} | remove_event {target, event} (gateway emülasyonu: platformun micro-op'u olay SİLEMEZ, eventValue: null yalnız değersiz ölü kayıt bırakır — bu op şemayı okuyup olayı diziden çıkarır ve sayfayı tam gövdeyle geri yazar) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler). target = elementId.");
17
+ /**
18
+ * Yazma yanıtındaki şema gövdesinin biçimi (B-55 · S-9). Varsayılan `outline`: gövde ajanın
19
+ * aynı turda kendi gönderdiği içeriğin yankısıdır, kanıt ise pointer'da ve `silentlyDropped`ta
20
+ * durur. Bayrağı bilmeyen ajan pahalı olanı ödemesin diye kırpma OPT-OUT'tur (aynı karar:
21
+ * `DS-004`, `datasource_configuration_options`).
22
+ */
23
+ const writeViewParam = z.enum(['outline', 'full']).optional().describe("Yanıttaki şema gövdesinin biçimi. outline (VARSAYILAN): komponent ağaçları iskelete iner (id/elementId/name/type/children), appliedPatch her op'un op+path'ini verir — 'yalnız o komponente mi dokunuldu' kanıtı JSON Pointer'dadır, 'kaydedildi mi' kanıtı silentlyDropped'tadır. full: platformun döndürdüğü gövde aynen gelir.");
24
+ /**
25
+ * `appliedPatch` zarfın altında da gelebilir (`{ data: { appliedPatch } }`), düz de.
26
+ */
27
+ function outlineWriteResponse(response) {
28
+ const inner = response.data;
29
+ const container = (inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : response);
30
+ if (Array.isArray(container.appliedPatch))
31
+ container.appliedPatch = outlineAppliedPatch(container.appliedPatch);
32
+ }
33
+ const opsParam = z.array(z.record(z.unknown())).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} | set_binding {target, prop, expression: '{{ Page.variables.x }}'} | add_component {parent (üst elementId ya da 'root'), index?, component} | remove_component {target} | move_component {target, parent, index?, position: {x, y}} | set_event {target, event, eventValue} | remove_event {target, event} (gateway emülasyonu: platformun micro-op'u olay SİLEMEZ, eventValue: null yalnız değersiz ölü kayıt bırakır — bu op şemayı okuyup olayı diziden çıkarır ve sayfayı tam gövdeyle geri yazar) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler) | resize_component {target, size?: {w,h}, appearance?: {widthType,widthString,heightType,heightString}, states?: {isWrapper,isScrollable,isSticky,isDroppable,isFloating,isQuickStack}, breakpoint?: 1024} (gateway emülasyonu: platformun micro-op'unda boyut YOKTUR ve move_component'e konan size sessizce düşer — bu op şemayı okuyup hedefin boyutunu değiştirir ve sayfayı tam gövdeyle geri yazar. Izgarada blocks[].size, flex'te appearance geçerlidir; ebeveynin düzeni yazılanı etkisiz kılıyorsa PAGE-019 uyarısı çıkar. states bir PROP DEĞİL düğüm alanıdır ve replace_prop onu yazamaz — kurulmuş bir düğümün isWrapper bayrağını değiştirmenin tek yolu buydu). set_prop_type {target, prop, propType: 'static'|'code'|'translation', ref?, data?} (gateway emülasyonu: replace_prop gönderilen ham değeri HER ZAMAN static Prop'una sarar ve sarmalanmış Prop nesnesini reddeder — kurulu bir etiketi çeviriye bağlamanın tek yolu düğümü remove_component + add_component ile yeniden kurmaktı ve o yol düğüme yazan setter iteratee'lerini sessizce öldürüyordu. Bu op şemayı okuyup prop'un TİPİNİ yerinde değiştirir ve sayfayı tam gövdeyle geri yazar. Taşıyıcı alan tipe göre: static → data, code ve translation → ref (runtime data'yı her turda ezer, anahtar yalnız ref'te yaşar). Eksik taşıyıcı istek gönderilmeden reddedilir; page_schema_validate aynı kararı verir). target = elementId.");
13
34
  /**
14
35
  * PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
15
36
  * komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
@@ -37,17 +58,6 @@ function collectTypes(nodes, into) {
37
58
  collectTypes(node.children, into);
38
59
  }
39
60
  }
40
- function invalidComponentNames(nodes) {
41
- const found = [];
42
- for (const node of nodes ?? []) {
43
- const name = node.name;
44
- if (typeof name === 'string' && name.length > 0 && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name.replace(/-/g, ''))) {
45
- found.push(`'${name}' (${node.elementId ?? '?'})`);
46
- }
47
- found.push(...invalidComponentNames(node.children));
48
- }
49
- return found;
50
- }
51
61
  function removeNodeFromTree(nodes, target) {
52
62
  for (let index = 0; index < nodes.length; index++) {
53
63
  if (nodes[index].elementId === target)
@@ -157,8 +167,113 @@ async function applyEventRemovals(platform, project, environment, applicationId,
157
167
  await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
158
168
  return removed;
159
169
  }
160
- export function registerPageTools(server, platform) {
170
+ function findParentNode(nodes, target) {
171
+ for (const node of nodes) {
172
+ if ((node.children ?? []).some(child => child.elementId === target))
173
+ return node;
174
+ const deeper = findParentNode(node.children ?? [], target);
175
+ if (deeper)
176
+ return deeper;
177
+ }
178
+ return undefined;
179
+ }
180
+ /**
181
+ * `resize_component` gateway emülasyonudur: platformun micro-op sözlüğünde boyut YOKTUR
182
+ * (`move_component` yalnız `position` uygular, gövdeye konan `size` 200 döner ve sessizce düşer —
183
+ * ölçüldü 09-04). Bu yüzden kurulmuş bir sayfada bir kartın yüksekliğini değiştirmenin tek yolu
184
+ * düğümü `remove_component` + `add_component` ile yeniden kurmaktı; o da ona yazan akış
185
+ * setter'larını sessizce öldürüyordu (FLOW-018). Op şemayı okur, hedefin boyutunu değiştirir ve
186
+ * sayfayı tam gövdeyle geri yazar — `remove_event` ile aynı desen.
187
+ *
188
+ * Izgarada boyut `blocks[].size`, flex'te `appearance` alanlarındadır; ikisi de kabul edilir ve
189
+ * ebeveynin düzeni okunanı etkisiz kılıyorsa uyarı üretilir (PAGE-015 ile aynı yaklaşım: reddetme,
190
+ * ama sessiz de kalma).
191
+ */
192
+ /**
193
+ * G-25 — `states` bir prop değil düğüm alanı: `replace_prop` yazamıyor, `move_component` yalnız
194
+ * konum uyguluyor ve `resize_component` (G-14) boyutla sınırlıydı. Kurulmuş bir düğümün
195
+ * `isWrapper` bayrağını değiştirmenin tek yolu düğümü silip yeniden kurmaktı; 09-05 koşusunda
196
+ * bunun bedeli 20 setter'ın sessizce kopmasıydı (G-21). Anahtar kümesi beyaz listeyle sınırlı:
197
+ * `states` açık uçlu bir sözlük ve uydurma bayrak sessizce kaydedilirdi.
198
+ */
199
+ const WRITABLE_STATES = new Set(['isWrapper', 'isScrollable', 'isSticky', 'isDroppable', 'isFloating', 'isQuickStack']);
200
+ async function applyResizes(platform, project, environment, applicationId, pageId, page, ops) {
201
+ const schema = JSON.parse(JSON.stringify(page.schema ?? []));
202
+ const applied = [];
203
+ const warnings = [];
204
+ for (const op of ops) {
205
+ const target = String(op.target ?? '');
206
+ const size = op.size && typeof op.size === 'object' ? op.size : undefined;
207
+ const appearance = op.appearance && typeof op.appearance === 'object' ? op.appearance : undefined;
208
+ const states = op.states && typeof op.states === 'object' ? op.states : undefined;
209
+ const breakpoint = Number(op.breakpoint ?? 1024);
210
+ if (!target)
211
+ throw new PlatformApiError("resize_component için 'target' zorunludur.", 400);
212
+ if (!size && !appearance && !states) {
213
+ throw new PlatformApiError(`resize_component '${target}' için 'size' ({ w, h } — ızgara), 'appearance' ({ widthType, widthString, heightType, heightString } — flex) ya da 'states' ({ isWrapper, isScrollable, isSticky, isDroppable, isFloating, isQuickStack }) verilmelidir; üçü de boş.`, 400);
214
+ }
215
+ if (states) {
216
+ const unknownStates = Object.keys(states).filter(key => !WRITABLE_STATES.has(key));
217
+ if (unknownStates.length > 0) {
218
+ throw new PlatformApiError(`resize_component '${target}': bilinmeyen states anahtarı (${unknownStates.join(', ')}). Yazılabilir bayraklar: ${[...WRITABLE_STATES].join(', ')}.`, 400);
219
+ }
220
+ }
221
+ const node = findNode(schema, target);
222
+ if (!node)
223
+ throw new PlatformApiError(`resize_component hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
224
+ const parent = findParentNode(schema, target);
225
+ const inFlex = parent !== undefined && isFlexContainer(parent);
226
+ if (size) {
227
+ const blocks = Array.isArray(node.blocks) ? node.blocks : [];
228
+ const block = blocks.find(entry => Number(entry?.breakpoint) === breakpoint);
229
+ if (!block) {
230
+ throw new PlatformApiError(`resize_component '${target}' düğümünde ${breakpoint} breakpoint'li blok yok (var olanlar: ${blocks.map(entry => entry?.breakpoint).join(', ') || 'hiç'}). Farklı bir breakpoint verin ya da düğümü add_component ile blokları kurulu olarak ekleyin.`, 400);
231
+ }
232
+ const current = (block.size ?? {});
233
+ block.size = { w: Number(size.w ?? current.w ?? 0), h: Number(size.h ?? current.h ?? 0) };
234
+ applied.push(`${target}.size=${JSON.stringify(block.size)}`);
235
+ if (inFlex) {
236
+ warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex kapsayıcı; orada yerleşimi 'blocks[].size' DEĞİL appearance (widthType/widthString/heightType/heightString) belirler. Boyut yazıldı ama ekranda etkisi olmayabilir — flex'te ölçü vermek için aynı op'a 'appearance' koyun.`);
237
+ }
238
+ }
239
+ if (states) {
240
+ node.states = { ...(node.states ?? {}), ...states };
241
+ applied.push(`${target}.states=${JSON.stringify(states)}`);
242
+ if (states.isWrapper === true) {
243
+ warnings.push(`PAGE-020 (uyarı): '${target}' düğümüne isWrapper:true yazıldı — runtime sarmalayıcı düğüme 'min-height: 80vh' veriyor ve bu, sayfanın altına yüzlerce piksel ölü boşluk ekleyebilir (ölçüldü 09-05: dokuz düğüm ~1600px). style ile ezilemez; gerekmiyorsa isWrapper:false bırakın.`);
244
+ }
245
+ }
246
+ if (appearance) {
247
+ node.appearance = { ...(node.appearance ?? {}), ...appearance };
248
+ applied.push(`${target}.appearance=${JSON.stringify(appearance)}`);
249
+ if (!inFlex && parent !== undefined) {
250
+ warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex DEĞİL; ızgarada yerleşimi 'blocks[].size' belirler ve appearance genişlik/yükseklik alanları etkisiz kalabilir. Izgarada ölçü vermek için aynı op'a 'size' koyun.`);
251
+ }
252
+ }
253
+ }
254
+ if (applied.length === 0)
255
+ return { applied, warnings };
256
+ const body = pageUpdateBody(page, { schema });
257
+ await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
258
+ return { applied, warnings };
259
+ }
260
+ async function applyPropTypes(platform, project, environment, applicationId, pageId, page, ops) {
261
+ const schema = JSON.parse(JSON.stringify(page.schema ?? []));
262
+ const { applied, missingTargets } = applyPropTypesToSchema(schema, ops);
263
+ if (missingTargets.length > 0) {
264
+ throw new PlatformApiError(`set_prop_type hedefi bulunamadı: '${missingTargets[0]}'. page_schema_get ile elementId'leri listeleyin.`, 400);
265
+ }
266
+ if (applied.length === 0)
267
+ return [];
268
+ const body = pageUpdateBody(page, { schema });
269
+ await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
270
+ return applied;
271
+ }
272
+ export function registerPageTools(server, platform, injectedScreenshotter) {
161
273
  const config = platform.config;
274
+ 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.");
275
+ /** Sayfa ve uç araçları aynı kimlik sözleşmesini paylaşır (G-20); çözüm tokenVault'ta yaşar. */
276
+ const resolveIdentity = resolveIdentityToken;
162
277
  server.registerTool('page_list', {
163
278
  title: 'Sayfaları listele',
164
279
  description: 'Uygulamanın sayfalarını (id, name, slug) listeler; hedef sayfayı bulmak için ilk adım.',
@@ -192,6 +307,7 @@ export function registerPageTools(server, platform) {
192
307
  name: page.name,
193
308
  slug: page.slug,
194
309
  browserTitle: page.browserTitle ?? '',
310
+ browserTitleKey: page.browserTitleKey ?? '',
195
311
  isHomepage: page.isHomepage === true,
196
312
  layoutId: page.layoutId ?? layout.id ?? null,
197
313
  layoutName: layout.name,
@@ -216,13 +332,20 @@ export function registerPageTools(server, platform) {
216
332
  pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
217
333
  view: z.enum(['summary', 'full']).optional().describe('summary (varsayılan): şema/layout ağaçları id/name/type özetine indirgenir. full: ham gövde — büyük olabilir'),
218
334
  resolveStyles: z.boolean().optional().describe("summary görünümünde her düğüme EFEKTİF stilini ekler: tema componentStyles → preset → düğümün kendi stili kaskadı çözülür (aibox'ta tipli stil grupları da derlenir ve TABAN katman olur). styleLayers hangi katmanın konuştuğunu söyler — 'bu kutu neden böyle görünüyor' sorusunun cevabı budur."),
219
- anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı olmadan gider — 'korumalı sayfa tokensız ne döner, şema sızıyor mu?' sorusunun cevabı budur. Korumalı sayfada middlewareBlocked bekleyin.")
335
+ 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."),
336
+ asUser: asUserParam
220
337
  },
221
338
  annotations: readOnly
222
- }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous }) => {
339
+ }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous, asUser }) => {
223
340
  let response;
341
+ if (anonymous === true && asUser !== undefined) {
342
+ 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.');
343
+ }
344
+ // page_screenshot çerez, bu tool başlık taşıyordu; çerez okuyan middleware ikincisinden
345
+ // yapısal olarak geçemiyordu. Kimlik artık iki kanalda birden gider (Y-02).
346
+ const identity = asUser === undefined ? {} : { headers: { authorization: `Bearer ${resolveIdentity(asUser)}` } };
224
347
  try {
225
- response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous });
348
+ response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous, mirrorAuthorizationCookie: true, ...identity });
226
349
  }
227
350
  catch (error) {
228
351
  if (error instanceof PlatformApiError && typeof error.status === 'number' && error.status >= 300) {
@@ -245,7 +368,7 @@ export function registerPageTools(server, platform) {
245
368
  const asNodes = (value) => (Array.isArray(value) ? value : []);
246
369
  const named = (value) => (Array.isArray(value) ? value : []).map(entry => ({ id: entry.id, name: entry.name, slug: entry.slug }));
247
370
  return {
248
- page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, isHomepage: body.isHomepage },
371
+ page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, browserTitleKey: body.browserTitleKey, isHomepage: body.isHomepage },
249
372
  events: body.events ?? [],
250
373
  layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(node => summarize(node, styleSource)) } : null,
251
374
  schema: asNodes(body.schema).map(node => summarize(node, styleSource)),
@@ -258,11 +381,11 @@ export function registerPageTools(server, platform) {
258
381
  : "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f); tam gövde gerekirse view:'full'. Bir düğümün ekranda GERÇEKTEN aldığı stili görmek için resolveStyles:true."
259
382
  };
260
383
  }));
261
- const screenshotter = new StudioScreenshotter(config);
384
+ const screenshotter = injectedScreenshotter ?? new StudioScreenshotter(config);
262
385
  const maxInlinePngBytes = 5 * 1024 * 1024;
263
386
  server.registerTool('page_screenshot', {
264
387
  title: 'Sayfanın ekran görüntüsünü al',
265
- description: "Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve hata/boş durum karesi yine döner — bu da bir doğrulamadır. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir.",
388
+ description: "Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve Studio'nun çizdiği hata durumu karesi döner — bu da bir doğrulamadır. YÜKLENMEMİŞ SAYFA ASLA ÇEKİLMEZ: kare, runtime çizilip mount akışları bitince alınır (yanıtta pageState + readyAfterMs); bütçe dolduğunda sayfa hâlâ iskelette ya da akış koşuyorsa çağrı ne görüldüğünü söyleyen bir hatayla döner, boş/yarım kare dönmez. Sayfa akışı bir adımda düşerse (patlayan ifade, hata dönen uç) yanıt runtimeErrors taşır ve hangi komponentin hangi prop'unun YAZILMADIĞINI söyler — kare 'çalışıyor' görünürken sessizce boş kalan komponenti ancak bu yakalar. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir.",
266
389
  inputSchema: {
267
390
  project: projectParam,
268
391
  environment: environmentParam,
@@ -273,13 +396,27 @@ export function registerPageTools(server, platform) {
273
396
  height: z.number().int().min(240).max(2160)
274
397
  }).optional().describe('Varsayılan 1280x800 (deviceScaleFactor 1)'),
275
398
  fullPage: z.boolean().optional().describe('true: kaydırılan kısım dahil tüm sayfa; varsayılan yalnız viewport karesi'),
276
- delayMs: z.number().int().min(0).max(10000).optional().describe('Animasyon/geç gelen veri için EK bekleme; 600 ms yerleşme beklemesi her zaman uygulanır'),
277
- anonymous: z.boolean().optional().describe("true: NG_AccessToken çerezi enjekte edilmez — anonim ziyaretçinin gördüğü kare. Korumalı sayfada middlewareBlocked bekleyin."),
399
+ 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)."),
400
+ 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."),
401
+ anonymous: z.boolean().optional().describe("true: hiçbir çerez enjekte edilmez — anonim ziyaretçinin gördüğü kare. Korumalı sayfada middlewareBlocked bekleyin."),
402
+ asUser: asUserParam,
403
+ 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)."),
404
+ 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."),
405
+ 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."),
406
+ actions: z.array(z.object({
407
+ action: z.enum(['set', 'click', 'wait']),
408
+ target: z.string().min(1).optional().describe("Hedefin elementId'si (page_schema_get); runtime düğümü data-element-id ile bulunur"),
409
+ value: z.unknown().optional().describe("'set' için yazılacak değer"),
410
+ ms: z.number().int().min(0).max(15000).optional().describe("'wait' için bekleme süresi")
411
+ })).max(20).optional().describe("Kare ALINMADAN ÖNCE sayfada koşturulacak etkileşim adımları: [{action:'set', target:'flt_month', value:'2022-10'}, {action:'click', target:'btn_sign_in'}]. 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez). Her adımdan sonra yerleşme beklenir ve yanıttaki actionsReport hedefin bulunup bulunmadığını ve adımdan sonra KAÇ AĞ İSTEĞİ çıktığını söyler — 'bağ kuruldu' ile 'çalışıyor' arasındaki farkı ancak bu ayırır (G-24)."),
278
412
  outPath: z.string().min(1).optional().describe('Verilirse PNG bu yerel yola yazılır ve inline görüntü DÖNMEZ; yalnız özet (yol, boyut) döner — büyük fullPage kareleri için')
279
413
  },
280
414
  annotations: readOnly
281
- }, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, anonymous, outPath }) => {
415
+ }, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, captureUnready, anonymous, asUser, cookies, layoutReport, actions, outPath, expectFailure }) => {
282
416
  try {
417
+ if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
418
+ 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.");
419
+ }
283
420
  if (!config.studioBaseUrl) {
284
421
  throw new PlatformApiError("page_screenshot için STUDIO_BASE_URL tanımlı değil. Studio arayüzünün kök adresini .env'e ekleyin (yerel geliştirmede http://localhost:3000, dağıtımda nginx adresi).");
285
422
  }
@@ -290,33 +427,103 @@ export function registerPageTools(server, platform) {
290
427
  }
291
428
  const url = `${config.studioBaseUrl}/${lane}/${platform.resolveProject(project)}/${applicationSlug}/${pageSlug}`;
292
429
  const size = viewport ?? { width: 1280, height: 800 };
430
+ const extraCookies = Object.entries(cookies ?? {}).map(([name, value]) => ({ name, value }));
431
+ const identity = anonymous === true ? 'anonymous' : asUser === undefined ? 'gateway' : 'asUser';
432
+ const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
293
433
  const shot = await screenshotter.capture({
294
434
  url,
295
435
  apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
296
- token: anonymous ? undefined : await platform.accessToken(),
436
+ token: anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken(),
437
+ ...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
297
438
  cookieUrl: config.studioBaseUrl,
298
439
  viewport: size,
299
440
  fullPage: fullPage === true,
300
- delayMs: delayMs ?? 0
441
+ delayMs: delayMs ?? 0,
442
+ ...(captureUnready === true ? { captureUnready: true } : {}),
443
+ ...(layoutReport === true ? { layoutReport: true } : {}),
444
+ ...(actions === undefined ? {} : { actions: actions })
301
445
  });
302
446
  const meta = {
303
447
  url,
304
448
  environment: resolvedEnvironment,
305
449
  viewport: size,
306
450
  fullPage: fullPage === true,
451
+ identity,
452
+ cookieNames,
307
453
  sizeBytes: shot.png.byteLength,
308
454
  elapsedMs: shot.elapsedMs,
455
+ ...(shot.contentHeight === undefined ? {} : { contentHeight: shot.contentHeight }),
456
+ ...(shot.fullPageTruncated
457
+ ? { fullPageTruncated: true, note: `Sayfa içeriği ${shot.contentHeight} px — kare 16384 px'de kırpıldı. Alt kısmı görmek için viewport'u kaydırılmış bir bölüme odaklayın ya da sayfayı bölerek doğrulayın.` }
458
+ : {}),
309
459
  ...(shot.apiStatus === undefined ? {} : { apiStatus: shot.apiStatus }),
310
460
  ...(shot.apiStatus !== undefined && shot.apiStatus >= 300
311
- ? { middlewareBlocked: true, note: 'Sayfanın veri isteği reddedildi — karede hata/boş durum görünüyor olabilir. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır (page_runtime_get ile karşılaştırın).' }
461
+ ? { 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?'." : ''}` }
462
+ : {}),
463
+ // P1-1 (S-3): eşzamanlı çekimler gateway içinde SERİ koşar; ajan yavaşlığın kuyruktan mı
464
+ // yoksa sayfadan mı geldiğini bilsin.
465
+ ...(shot.queuedMs === undefined ? {} : { queuedMs: shot.queuedMs }),
466
+ ...(shot.contextRecycled === true
467
+ ? { contextRecycled: true, contextRecycledNote: 'Önceki çekim tarayıcı bağlamını bozdu; bu kare TEMİZ bir tarayıcıyla alındı — oturum çerezleri yeniden enjekte edildi.' }
312
468
  : {}),
313
- ...(shot.renderWaitTimedOut
314
- ? { renderWaitTimedOut: true, note: "Sayfanın veri isteği gözlenmedi — kare hata/boş durumu gösteriyor olabilir; STUDIO_BASE_URL ve slug'ları doğrulayın." }
315
- : {})
469
+ // S-8 (09-08): soğuk Studio'da navigasyon zaman aşımı ajanın çağrıyı tekrarlamasını
470
+ // gerektiriyordu; artık gateway bir kez kendi deniyor ve karenin neden geciktiğini söylüyor.
471
+ ...(shot.navigationRetried === true
472
+ ? { navigationRetried: true, navigationRetriedNote: 'İlk navigasyon zaman aşımına düştü ve kare ikinci denemede alındı — Studio soğuktu (ilk istek Vite derlemesini ısıtır). Sonraki çekimler hızlı olmalı; olmuyorsa delayMs verin, o değer navigasyon bütçesini de uzatır.' }
473
+ : {}),
474
+ // B-6 (2026-09-07): kare yalnız sayfa hazırken alınır; hangi kanıtla hazır sayıldığı yazılır.
475
+ pageState: shot.pageState,
476
+ readyAfterMs: shot.readyAfterMs,
477
+ ...(shot.finalUrl === undefined ? {} : { finalUrl: shot.finalUrl }),
478
+ ...(shot.pageState === 'ready-no-flows'
479
+ ? { 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.' }
480
+ : shot.pageState === 'ready-no-counter'
481
+ ? { 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ı.' }
482
+ : shot.pageState === 'error'
483
+ ? { pageStateNote: 'Studio sayfa için hata/bulunamadı durumunu çizdi; kare o durumu gösterir. apiStatus ve slug\'ları doğrulayın.' }
484
+ : shot.pageState === 'redirected'
485
+ ? { pageStateNote: `İstenen adres runtime dışına yönlendi (finalUrl). Route guard ya da Studio giriş ekranı olabilir; kare yönlendirilen sayfayı gösterir.` }
486
+ : shot.pageState === 'not-ready'
487
+ ? { pageStateNote: 'TEŞHİS KARESİ (captureUnready): bütçe dolduğunda sayfa hazır değildi; kare iskelet ya da yarım durumu gösterebilir, doğrulama için kullanmayın.' }
488
+ : {}),
489
+ ...(shot.renderWaitTimedOut && shot.pageState === 'not-ready'
490
+ ? { renderWaitTimedOut: true, note: "Sayfanın veri isteği gözlenmedi — STUDIO_BASE_URL ve slug'ları doğrulayın." }
491
+ : {}),
492
+ ...(shot.flowsSettled === undefined
493
+ ? {}
494
+ : shot.flowsSettled
495
+ ? { flowsSettled: true }
496
+ : { flowsSettled: false, flowsSettledNote: 'Sayfanın akışları bütçe içinde bitmedi; kare akış koşarken alındı (captureUnready). Akış gerçekten uzunsa delayMs ile bütçeyi büyütün.' }),
497
+ ...(shot.actions === undefined ? {} : { actionsReport: shot.actions }),
498
+ // S-4: kasıtlı negatif test de akış hatası bırakır ve ölçüm aracı onu plansız hata
499
+ // sayıyordu. Çağıran beklediğini söylediğinde satır ayrı sayılır.
500
+ ...(expectFailure === true ? { expectedFailure: true } : {}),
501
+ ...(shot.runtimeErrors === undefined
502
+ ? {}
503
+ : {
504
+ runtimeErrors: shot.runtimeErrors,
505
+ 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.`
506
+ }),
507
+ ...(shot.actions?.some(outcome => !outcome.found)
508
+ ? { actionsNote: `Bulunamayan hedef var: ${shot.actions.filter(outcome => !outcome.found).map(outcome => outcome.target ?? '?').join(' · ')}. elementId'leri page_schema_get ile doğrulayın; runtime düğümü data-element-id ile bulunuyor.` }
509
+ : {}),
510
+ ...(shot.actions?.some(outcome => outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0)
511
+ ? { actionsNote: `Uygulandı ama HİÇ AĞ İSTEĞİ çıkmadı ve HİÇBİR AKIŞ KOŞMADI: ${shot.actions.filter(outcome => outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0).map(outcome => `${outcome.action} '${outcome.target ?? '?'}'`).join(' · ')}. Olay bağı şemada görünse bile akış koşmamış olabilir — bağın gerçekten kurulu olduğunu page_runtime_get ile, akışın gövdesini flow_get ile doğrulayın.` }
512
+ : {}),
513
+ ...(shot.layout === undefined
514
+ ? {}
515
+ : shot.layout.length === 0
516
+ ? { 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." }
517
+ : {
518
+ layoutSummary: summarizeLayout(shot.layout, size),
519
+ ...(shot.layout.length <= 100 ? { layout: shot.layout } : { layoutTruncatedTo: 'summary' })
520
+ })
316
521
  };
317
522
  if (outPath) {
318
523
  const absolute = resolve(outPath);
319
524
  try {
525
+ // B-6: klasör yoksa ENOENT ile düşüyordu; koşu ajanı her seferinde klasörü elle açıyordu.
526
+ await mkdir(dirname(absolute), { recursive: true });
320
527
  await writeFile(absolute, shot.png);
321
528
  }
322
529
  catch (error) {
@@ -376,10 +583,16 @@ export function registerPageTools(server, platform) {
376
583
  server.registerTool('page_schema_validate', {
377
584
  title: 'Sayfa şemasını doğrula (kuru çalıştırma)',
378
585
  description: "Micro-op'ları kaydetmeden şemaya uygular ve doğrulama hatalarını (suggestions ile) döndürür; page_schema_patch öncesi planı test etmek için. Gövdeye page_schema_patch ile AYNI normalizasyonu uygular (Prop sarmalı, validation biçimi) — kuru koşu ile yazma aynı gövdeyi görür.",
379
- inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional() },
586
+ inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional(), operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için") },
380
587
  annotations: readOnly
381
- }, handle(config, async ({ project, environment, applicationId, pageId, ops }) => {
382
- const opRecords = (ops ?? []).map(op => op);
588
+ }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations }) => {
589
+ const allOps = microOps('page_schema_validate', ops, operations).map(op => op);
590
+ const propTypeIssues = setPropTypeErrors(allOps.filter(entry => entry.op === 'set_prop_type'));
591
+ if (propTypeIssues.length > 0)
592
+ throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
593
+ // Gateway emülasyonu olan op'lar platformun doğrulama ucunu tanımadığı için gövdeden ayrılır;
594
+ // kuru koşu ile yazma AYNI kararı vermeli (P0-2 Step 3).
595
+ const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
383
596
  const normalized = normalizeSchemaOps(opRecords);
384
597
  const validationIssues = schemaOpValidationErrors(opRecords);
385
598
  const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
@@ -396,22 +609,34 @@ export function registerPageTools(server, platform) {
396
609
  }));
397
610
  server.registerTool('page_schema_patch', {
398
611
  title: 'Sayfa şemasına patch uygula',
399
- description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. expectedUpdatedAt verilirse sayfa o okumadan beri değişmişse istek reddedilir (stale plan koruması). Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler.",
612
+ description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. expectedUpdatedAt verilirse sayfa o okumadan beri değişmişse istek reddedilir (stale plan koruması). Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler; gövdesi VARSAYILAN OLARAK iskelettir (eklenen düğüm id/type/children'a iner) — tam gövde için view:'full'. Op'un gerçekten kaydedilip kaydedilmediğini gövde değil silentlyDropped söyler.",
400
613
  inputSchema: {
401
614
  project: projectParam,
402
615
  environment: environmentParam,
403
616
  applicationId: guid,
404
617
  pageId: guid,
405
- ops: opsParam,
406
- expectedUpdatedAt: z.string().optional().describe("page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz")
618
+ ops: opsParam.optional(),
619
+ operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"),
620
+ expectedUpdatedAt: z.string().optional().describe("page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz"),
621
+ allowFlowDeletion: z.boolean().optional().describe("true: bu yama bir akışı referanssız bırakacaksa yine de uygulanır. Varsayılan false — platform referanssız akışı KALICI siler ve gateway isteği önden reddeder (G-21). Silinen akışlar yanıtta deletedFlows ile listelenir."),
622
+ view: writeViewParam
407
623
  },
408
624
  annotations: updates
409
- }, handle(config, async ({ project, environment, applicationId, pageId, ops, expectedUpdatedAt }) => {
410
- const allOps = ops.map(op => op);
625
+ }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, expectedUpdatedAt, allowFlowDeletion, view }) => {
626
+ const allOps = microOps('page_schema_patch', ops, operations).map(op => op);
411
627
  const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
412
- const opRecords = allOps.filter(entry => entry.op !== 'remove_event');
628
+ const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
629
+ const propTypeOps = allOps.filter(entry => entry.op === 'set_prop_type');
630
+ const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
631
+ const propTypeIssues = setPropTypeErrors(propTypeOps);
632
+ if (propTypeIssues.length > 0)
633
+ throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
413
634
  const containerOps = opRecords
414
635
  .filter(entry => (entry.op === 'remove_component' || entry.op === 'move_component') && entry.target === PAGE_CONTAINER_ELEMENT_ID);
636
+ const sizedMoves = opRecords.filter(entry => entry.op === 'move_component' && entry.size !== undefined);
637
+ if (sizedMoves.length > 0) {
638
+ throw new PlatformApiError(`move_component yalnız 'position' uygular; gövdeye konan 'size' platformda SESSİZCE düşer (ölçüldü 2026-09-04, appliedPatch'te yalnız /blocks/0/position görünür). Boyut için resize_component kullanın: { op: 'resize_component', target: '<elementId>', size: { w, h } } — flex kapsayıcı içindeyse 'appearance'. Etkilenen hedefler: ${sizedMoves.map(entry => entry.target).join(' · ')}`, 400);
639
+ }
415
640
  if (containerOps.length > 0) {
416
641
  throw new PlatformApiError(`'${PAGE_CONTAINER_ELEMENT_ID}' sayfanın kök kapsayıcısıdır; silinmez ve taşınmaz (PAGE-006) — silinirse sayfa iskeleti bozulur ve Studio komponentleri kapsayıcı dışında bırakır. Komponent kaldırmak için remove_component'i kapsayıcının İÇİNDEKİ elementId'lerle kullanın (page_schema_get ile listeleyin).`, 400);
417
642
  }
@@ -455,23 +680,27 @@ export function registerPageTools(server, platform) {
455
680
  if (unknown.length > 0)
456
681
  throw new PlatformApiError(`PAGE-008 — geçersiz olay adı (istek gönderilmedi): ${unknown.join(' · ')}`, 400);
457
682
  }
683
+ // Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
684
+ // silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
685
+ let deletedFlows = [];
686
+ if (opRecords.some(entry => ['remove_component', 'set_event', 'remove_event'].includes(String(entry.op)))) {
687
+ try {
688
+ const page = await readPage();
689
+ deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
690
+ }
691
+ catch {
692
+ deletedFlows = [];
693
+ }
694
+ if (deletedFlows.length > 0 && allowFlowDeletion !== true) {
695
+ throw new PlatformApiError(orphanedFlowMessage(deletedFlows), 400);
696
+ }
697
+ }
458
698
  const added = opRecords
459
699
  .filter(entry => entry.op === 'add_component')
460
700
  .map(entry => entry.component);
461
- const badWidgets = invalidWidgetTypeNodes(added, 'PAGE-012');
462
- if (badWidgets.length > 0)
463
- throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
464
- const badNames = invalidComponentNames(added);
465
- if (badNames.length > 0) {
466
- throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badNames.join(' · ')}. `
467
- + 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; boşluklu ya da geçersiz bir ad '
468
- + 'SAYFADAKİ TÜM ifadeleri "Missing initializer in const declaration" ile öldürür (layout adları her sayfaya sızar). '
469
- + 'name alanını elementId ile aynı yaz; okunabilir Türkçe başlık için props.title / props.text / props.label kullan.', 400);
470
- }
471
- const catalog = added.length > 0
472
- ? await applyCatalogLayoutRules(platform, project, environment, applicationId, added)
473
- : { clamped: [], heights: [] };
474
- const formWarnings = formBindingWarnings(opRecords);
701
+ assertScreenWriteGuards('page', { schema: added });
702
+ const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, added);
703
+ const formWarnings = [...formBindingWarnings(opRecords), ...formSubmitButtonWarnings(opRecords)];
475
704
  let flowWarnings = [];
476
705
  let layoutWarnings = [];
477
706
  if (opRecords.some(entry => entry.op === 'remove_component')) {
@@ -482,6 +711,16 @@ export function registerPageTools(server, platform) {
482
711
  catch {
483
712
  flowWarnings = [];
484
713
  }
714
+ // Ters yön (FLOW-018): silinen düğüme YAZAN setter'lar. Akış gövdeleri şemada değil, ayrı
715
+ // uçta; okunamazsa uyarı üretmeyiz ama yama yine geçer.
716
+ try {
717
+ const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
718
+ const page = await readPage();
719
+ flowWarnings = [...flowWarnings, ...staleSetterWarnings(hosted?.data ?? [], opRecords, page.schema)];
720
+ }
721
+ catch {
722
+ // akış listesi okunamadı
723
+ }
485
724
  }
486
725
  let nestedAfterOps = [];
487
726
  const simulatable = opRecords.some(entry => ['add_component', 'move_component'].includes(String(entry.op))
@@ -498,7 +737,8 @@ export function registerPageTools(server, platform) {
498
737
  ...pageContainerPositionWarning(page.schema),
499
738
  ...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
500
739
  ...stringStyleWarning(stringStyleNodes(simulated)),
501
- ...inertFlexPropWarning(inertFlexPropNodes(simulated))
740
+ ...inertFlexPropWarning(inertFlexPropNodes(simulated)),
741
+ ...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')
502
742
  ];
503
743
  }
504
744
  catch {
@@ -512,29 +752,62 @@ export function registerPageTools(server, platform) {
512
752
  : [];
513
753
  if (removeEventOps.length > 0)
514
754
  cachedPage = undefined;
755
+ const resized = resizeOps.length > 0
756
+ ? await applyResizes(platform, project, environment, applicationId, pageId, await readPage(), resizeOps)
757
+ : { applied: [], warnings: [] };
758
+ if (resizeOps.length > 0)
759
+ cachedPage = undefined;
760
+ const propTypes = propTypeOps.length > 0
761
+ ? await applyPropTypes(platform, project, environment, applicationId, pageId, await readPage(), propTypeOps)
762
+ : [];
763
+ if (propTypeOps.length > 0)
764
+ cachedPage = undefined;
515
765
  const response = opRecords.length > 0
516
766
  ? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: opRecords } })) ?? {})
517
767
  : { valid: true };
768
+ if (view !== 'full')
769
+ outlineWriteResponse(response);
518
770
  if (removedEvents.length > 0)
519
771
  response.removedEvents = removedEvents;
772
+ if (propTypes.length > 0)
773
+ response.propTypes = propTypes;
774
+ if (resized.applied.length > 0)
775
+ response.resized = resized.applied;
776
+ if (resized.warnings.length > 0)
777
+ response.resizeWarnings = resized.warnings;
778
+ if (deletedFlows.length > 0)
779
+ response.deletedFlows = deletedFlows;
520
780
  if (formWarnings.length > 0)
521
781
  response.formWarnings = formWarnings;
522
782
  if (flowWarnings.length > 0)
523
783
  response.flowWarnings = flowWarnings;
524
784
  if (layoutWarnings.length > 0)
525
785
  response.layoutWarnings = layoutWarnings;
526
- return withNormalization(response, [
786
+ // G-23 micro-op ayağı: yama 200 döner ve `appliedPatch` op'un ŞEMAYA yazıldığını söyler, ama
787
+ // alanın gerçekten KAYDEDİLDİĞİNİ söylemez. Yamadan sonra şemayı bir kez okuyup op'un yazmak
788
+ // istediğiyle kayıtlı olanı karşılaştırıyoruz. Okuma başarısız olursa yama yine geçerlidir.
789
+ const contentOps = opRecords.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
790
+ let opDrops = [];
791
+ if (contentOps.length > 0) {
792
+ try {
793
+ const saved = await fetchPage(platform, project, environment, applicationId, pageId);
794
+ opDrops = schemaOpDrops(saved.schema, contentOps);
795
+ }
796
+ catch {
797
+ opDrops = [];
798
+ }
799
+ }
800
+ return withWriteDiff(withNormalization(response, [
527
801
  ...rootAddsNote(retargeted, PAGE_CONTAINER_ELEMENT_ID),
528
802
  ...componentDefaultsNote(normalized.components),
529
803
  ...validationNote(normalized.validations),
530
- ...blockSizeNote(catalog.clamped),
531
- ...heightTypeNote(catalog.heights),
804
+ ...catalogNotes,
532
805
  ...replacePropNote(normalized.props)
533
- ]);
806
+ ]), opDrops);
534
807
  }));
535
808
  server.registerTool('page_create', {
536
809
  title: 'Sayfa oluştur',
537
- description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (doğrulama duvarı + canlı canvas orada). roleIds için role_list, layoutId için layout_list kullanın.",
810
+ description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (canlı canvas ve hedefli düzeltme orada). Şemayı burada gönderirseniz 2026-09-08'den beri AYNI doğrulama duvarından geçer: tanınmayan prop adı ve geçersiz enum değeri 422 ile reddedilir ve sayfa HİÇ açılmaz. roleIds için role_list, layoutId için layout_list kullanın. Yanıttaki schema VARSAYILAN OLARAK iskelettir (çözülmüş layout düğümleri dâhil id/type/children'a iner) — tam gövde için view:'full'; gönderdiğiniz bir alan kaydedilmediyse silentlyDropped söyler.",
538
811
  inputSchema: {
539
812
  project: projectParam,
540
813
  environment: environmentParam,
@@ -542,30 +815,24 @@ export function registerPageTools(server, platform) {
542
815
  name: z.string().min(1),
543
816
  slug: z.string().min(1).describe("URL yolu, örn. 'orders' ya da 'orders/detail'"),
544
817
  browserTitle: z.string().optional(),
818
+ browserTitleKey: z.string().optional().describe("Sekme başlığının çeviri anahtarı. Verilirse başlık çeviri sözlüğünden çözülür ve language-selector ile dil değişince sayfa yenilenmeden güncellenir; boşsa browserTitle olduğu gibi kullanılır. Anahtarlar için translation_list."),
545
819
  layoutId: guid.optional(),
546
820
  roleIds: z.array(guid).optional().describe('Sayfayı görebilecek roller; boş = herkese açık'),
547
821
  middlewareIds: z.array(guid).optional(),
548
822
  events: eventsParam.optional(),
549
- schema: schemaParam.optional()
823
+ schema: schemaParam.optional(),
824
+ view: writeViewParam
550
825
  },
551
826
  annotations: writes
552
- }, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, layoutId, roleIds, middlewareIds, events, schema }) => {
553
- const badEvents = invalidScreenEventNames(events, 'PAGE-011');
554
- if (badEvents.length > 0)
555
- throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
827
+ }, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, events, schema, view }) => {
556
828
  const container = ensurePageContainer(schema ?? []);
557
- const badWidgets = invalidWidgetTypeNodes(container.schema, 'PAGE-012');
558
- if (badWidgets.length > 0)
559
- throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
560
- const nested = nestedRepeaterNodes(container.schema);
561
- if (nested.length > 0)
562
- throw new PlatformApiError(nestedRepeaterMessage(nested), 400);
829
+ assertScreenWriteGuards('page', { schema: container.schema, events });
563
830
  const stringStyles = stringStyleNodes(container.schema);
564
831
  const fixed = normalizeComponentDefaults(container.schema);
565
832
  const reset = resetPageContainerPosition(container.schema);
566
- const catalog = (schema ?? []).length > 0
567
- ? await applyCatalogLayoutRules(platform, project, environment, applicationId, container.schema)
568
- : { clamped: [], heights: [] };
833
+ const catalogNotes = (schema ?? []).length > 0
834
+ ? await screenCatalogNotes(platform, project, environment, applicationId, container.schema)
835
+ : [];
569
836
  const moved = reorderSchemaBlocks(container.schema);
570
837
  const gaps = (schema ?? []).length > 0 ? verticalGapWarnings(container.schema, 'PAGE-010 (uyarı)') : [];
571
838
  const response = await platform.tenant('POST', project, `/pages/${applicationId}`, {
@@ -574,6 +841,7 @@ export function registerPageTools(server, platform) {
574
841
  name,
575
842
  slug,
576
843
  browserTitle: browserTitle ?? '',
844
+ browserTitleKey: browserTitleKey ?? '',
577
845
  layoutId: layoutId ?? null,
578
846
  roleIds: roleIds ?? [],
579
847
  middlewareIds: middlewareIds ?? [],
@@ -581,8 +849,20 @@ export function registerPageTools(server, platform) {
581
849
  schema: container.schema
582
850
  }
583
851
  });
584
- const normalized = withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...blockSizeNote(catalog.clamped), ...heightTypeNote(catalog.heights), ...reorderNote(moved)]);
585
- const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema))];
852
+ const saved = (response && typeof response === 'object' && !Array.isArray(response)
853
+ ? (response.data ?? response)
854
+ : {});
855
+ // Kırpmadan ÖNCE doğrulama yolu (B-55): yanıt gövdesi iskelete inince "gönderdiğim prop
856
+ // kaydedildi mi" sorusunu gövde değil bu diff cevaplar. `layout_create`'in yaptığının sayfa
857
+ // ikizi; Prop sarmalı açılmazsa doğru kaydedilmiş her prop düşmüş görünür (G-28).
858
+ const drops = (schema ?? []).length > 0
859
+ ? silentlyDropped({ schema: unwrapProps(container.schema) }, { schema: unwrapProps(saved.schema) })
860
+ : [];
861
+ if (view !== 'full' && Array.isArray(saved.schema)) {
862
+ saved.schema = saved.schema.map(node => summarize(node));
863
+ }
864
+ const normalized = withWriteDiff(withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), drops);
865
+ const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema)), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
586
866
  if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
587
867
  return normalized;
588
868
  return { ...normalized, layoutWarnings: createWarnings };
@@ -598,6 +878,7 @@ export function registerPageTools(server, platform) {
598
878
  name: z.string().min(1).optional(),
599
879
  slug: z.string().min(1).optional(),
600
880
  browserTitle: z.string().optional(),
881
+ browserTitleKey: z.string().optional().describe("Sekme başlığının çeviri anahtarı. Verilirse başlık çeviri sözlüğünden çözülür ve language-selector ile dil değişince sayfa yenilenmeden güncellenir; boşsa browserTitle olduğu gibi kullanılır. Anahtarlar için translation_list."),
601
882
  layoutId: guid.nullable().optional().describe('null: layout bağını kaldırır'),
602
883
  roleIds: z.array(guid).optional(),
603
884
  middlewareIds: z.array(guid).optional(),
@@ -607,31 +888,15 @@ export function registerPageTools(server, platform) {
607
888
  annotations: updates
608
889
  }, handle(config, async ({ project, environment, applicationId, pageId, ...changes }) => {
609
890
  const overrides = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== undefined));
610
- const badEvents = invalidScreenEventNames(overrides.events, 'PAGE-011');
611
- if (badEvents.length > 0)
612
- throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
613
- const badWidgets = invalidWidgetTypeNodes(overrides.schema, 'PAGE-012');
614
- if (badWidgets.length > 0)
615
- throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
616
- const nestedUpdate = nestedRepeaterNodes(overrides.schema);
617
- if (nestedUpdate.length > 0)
618
- throw new PlatformApiError(nestedRepeaterMessage(nestedUpdate), 400);
891
+ assertScreenWriteGuards('page', { schema: overrides.schema, events: overrides.events });
619
892
  const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
620
- const badSchemaNames = invalidComponentNames(overrides.schema);
621
- if (badSchemaNames.length > 0) {
622
- throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badSchemaNames.join(' · ')}. `
623
- + 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; geçersiz bir ad sayfadaki TÜM ifadeleri öldürür. '
624
- + 'name = elementId yazın.', 400);
625
- }
626
893
  const fixed = normalizeComponentDefaults(overrides.schema);
627
894
  const reset = overrides.schema === undefined ? [] : resetPageContainerPosition(overrides.schema);
628
- const catalog = overrides.schema === undefined
629
- ? { clamped: [], heights: [] }
630
- : await applyCatalogLayoutRules(platform, project, environment, applicationId, overrides.schema);
895
+ const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, overrides.schema);
631
896
  const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
632
897
  const gaps = overrides.schema === undefined
633
898
  ? []
634
- : [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema))];
899
+ : [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema)), ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
635
900
  const body = pageUpdateBody(current, overrides);
636
901
  await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
637
902
  const summary = {
@@ -641,7 +906,7 @@ export function registerPageTools(server, platform) {
641
906
  };
642
907
  if (gaps.length > 0)
643
908
  summary.layoutWarnings = gaps;
644
- return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...blockSizeNote(catalog.clamped), ...heightTypeNote(catalog.heights), ...reorderNote(moved)]);
909
+ return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]);
645
910
  }));
646
911
  server.registerTool('page_clone', {
647
912
  title: 'Sayfa klonla',