@appaflytech/zeyrek-ai-mcp 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -2
- package/.env.production.example +9 -1
- package/README.md +14 -38
- package/dist/config.js +36 -3
- package/dist/knowledge/backendConstraints.js +982 -0
- package/dist/knowledge/componentCatalog.js +549 -92
- package/dist/knowledge/componentEvents.js +120 -25
- package/dist/knowledge/coverage.js +43 -0
- package/dist/knowledge/flowGuards.js +30 -0
- package/dist/knowledge/functionGuards.js +134 -0
- package/dist/knowledge/guideSections.js +49 -0
- package/dist/knowledge/hydrate.js +97 -0
- package/dist/knowledge/languageCodes.js +36 -0
- package/dist/knowledge/languages.js +105 -0
- package/dist/knowledge/layoutSummary.js +10 -0
- package/dist/knowledge/localization.js +51 -0
- package/dist/knowledge/normalize.js +500 -16
- package/dist/knowledge/outlines.js +167 -12
- package/dist/knowledge/policy.js +14 -5
- package/dist/knowledge/propTypes.js +80 -0
- package/dist/knowledge/rules.js +961 -44
- package/dist/knowledge/schemaGuard.js +199 -0
- package/dist/knowledge/styleResolve.js +63 -4
- package/dist/observability/trace.js +27 -3
- package/dist/platform/csv.js +123 -0
- package/dist/platform/errors.js +107 -6
- package/dist/platform/http.js +34 -4
- package/dist/platform/patch.js +27 -0
- package/dist/platform/refs.js +104 -0
- package/dist/platform/responses.js +25 -6
- package/dist/platform/screenshot.js +544 -16
- package/dist/platform/secrets.js +40 -0
- package/dist/platform/tokenVault.js +89 -0
- package/dist/platform/writeDiff.js +281 -0
- package/dist/resources/examples/flow-call-function.json +66 -0
- package/dist/resources/examples/function-total.json +70 -0
- package/dist/resources/examples/layout-app-shell.json +1 -1
- package/dist/resources/examples/microfunction-node-sum.json +56 -0
- package/dist/resources/examples/microservice-table-list.json +2 -4
- package/dist/resources/examples/page-dynamic-form.json +3 -3
- package/dist/resources/examples/page-flex-sections.json +99 -0
- package/dist/resources/examples/page-sign-in.json +4 -3
- package/dist/resources/guides/component.md +81 -8
- package/dist/resources/guides/entity.md +712 -20
- package/dist/resources/guides/expressions.md +47 -2
- package/dist/resources/guides/external-component.md +9 -0
- package/dist/resources/guides/functions.md +262 -0
- package/dist/resources/guides/getting-started.md +82 -7
- package/dist/resources/guides/layout.md +53 -10
- package/dist/resources/guides/merge.md +231 -0
- package/dist/resources/guides/microfunction.md +166 -0
- package/dist/resources/guides/microservice.md +287 -15
- package/dist/resources/guides/middleware.md +89 -1
- package/dist/resources/guides/modify-existing.md +23 -0
- package/dist/resources/guides/page.md +402 -49
- package/dist/resources/guides/translation.md +150 -8
- package/dist/resources/guides/view.md +16 -1
- package/dist/resources/guides/workflow.md +199 -28
- package/dist/resources/knowledge.js +5 -0
- package/dist/resources/rules.js +27 -0
- package/dist/resources/schema-fingerprint.json +8 -0
- package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
- package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
- package/dist/resources/schemas/CreateBulkRowCommand.json +36 -0
- package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
- package/dist/resources/schemas/CreateEntityCommand.json +52 -12
- package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
- package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
- package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
- package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
- package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
- package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
- package/dist/resources/schemas/CreateMicroserviceCommand.json +145 -3
- package/dist/resources/schemas/CreateQueryCommand.json +56 -3
- package/dist/resources/schemas/CreateRowCommand.json +38 -0
- package/dist/resources/schemas/CreateViewCommand.json +13 -10
- package/dist/resources/schemas/CreateWorkflowCommand.json +106 -14
- package/dist/resources/schemas/DataJobDto.json +267 -0
- package/dist/resources/schemas/ExportRowsCommand.json +172 -0
- package/dist/resources/schemas/Filter.json +3 -1
- package/dist/resources/schemas/GetRowsQuery.json +6 -1
- package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
- package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
- package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
- package/dist/resources/schemas/ImportPlanDto.json +260 -0
- package/dist/resources/schemas/ImportSampleDto.json +90 -0
- package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +135 -1
- package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
- package/dist/resources/schemas/PreviewWorkflowCommand.json +45 -9
- package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
- package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
- package/dist/resources/schemas/RunImportCommand.json +62 -0
- package/dist/resources/schemas/StepDto.json +34 -3
- package/dist/resources/schemas/UpdateBulkRowCommand.json +36 -0
- package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/UpdateEntityCommand.json +52 -11
- package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
- package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
- package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +147 -4
- package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
- package/dist/resources/schemas/UpdateRowCommand.json +40 -0
- package/dist/resources/schemas/UpdateViewCommand.json +15 -10
- package/dist/resources/schemas/UpdateWorkflowCommand.json +106 -14
- package/dist/server.js +17 -1
- package/dist/tools/access.js +3 -3
- package/dist/tools/activities.js +40 -0
- package/dist/tools/applications.js +1 -1
- package/dist/tools/automations.js +76 -34
- package/dist/tools/common.js +55 -0
- package/dist/tools/components.js +16 -7
- package/dist/tools/datasources.js +83 -21
- package/dist/tools/dependencies.js +1 -1
- package/dist/tools/deployments.js +34 -2
- package/dist/tools/dtos.js +25 -12
- package/dist/tools/entities.js +113 -28
- package/dist/tools/environments.js +52 -0
- package/dist/tools/externalComponents.js +9 -3
- package/dist/tools/files.js +57 -42
- package/dist/tools/flows.js +205 -13
- package/dist/tools/folders.js +7 -2
- package/dist/tools/functions.js +331 -0
- package/dist/tools/globalFunctions.js +63 -0
- package/dist/tools/importSchemas.js +79 -0
- package/dist/tools/imports.js +241 -0
- package/dist/tools/layouts.js +103 -29
- package/dist/tools/merges.js +113 -0
- package/dist/tools/microfunctions.js +155 -0
- package/dist/tools/microservices.js +270 -40
- package/dist/tools/organizations.js +5 -6
- package/dist/tools/pages.js +421 -104
- package/dist/tools/platformInfo.js +155 -11
- package/dist/tools/projects.js +183 -33
- package/dist/tools/queries.js +32 -10
- package/dist/tools/revisions.js +94 -0
- package/dist/tools/rows.js +484 -50
- package/dist/tools/screenWrite.js +68 -0
- package/dist/tools/screens.js +28 -0
- package/dist/tools/templates.js +32 -5
- package/dist/tools/translations.js +24 -8
- package/dist/tools/views.js +20 -10
- package/dist/tools/workflows.js +56 -21
- package/package.json +7 -2
package/dist/tools/pages.js
CHANGED
|
@@ -1,15 +1,57 @@
|
|
|
1
|
-
import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
|
|
2
|
-
import { orphanedFlowWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive, microOps, isBranchName } from './common.js';
|
|
2
|
+
import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, staleSetterWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, isFlexContainer, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, withNormalization, parseVariableEntries, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
|
|
3
|
+
import { functionSignature } from '../knowledge/functionGuards.js';
|
|
4
|
+
import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, outlineAppliedPatch, findNode, flowPath } from './screens.js';
|
|
5
|
+
import { fetchComponentEventNames } from './components.js';
|
|
6
|
+
import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
|
|
7
|
+
import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
|
|
8
|
+
import { schemaOpDrops, silentlyDropped, unwrapProps, withWriteDiff } from '../platform/writeDiff.js';
|
|
6
9
|
import { PlatformApiError } from '../platform/errors.js';
|
|
7
10
|
import { handle, toolResult, toolError } from '../platform/responses.js';
|
|
8
11
|
import { StudioScreenshotter } from '../platform/screenshot.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
12
|
+
import { summarizeLayout } from '../knowledge/layoutSummary.js';
|
|
13
|
+
import { normalizeLanguageCode } from '../knowledge/languageCodes.js';
|
|
14
|
+
import { readProjectLanguages } from '../knowledge/languages.js';
|
|
15
|
+
import { resolveIdentityToken } from '../platform/tokenVault.js';
|
|
16
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { dirname, resolve } from 'node:path';
|
|
18
|
+
import { applyPropTypesToSchema, setPropTypeErrors } from '../knowledge/propTypes.js';
|
|
11
19
|
import { z } from 'zod';
|
|
12
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Runtime paketindeki `appFunctions`/`pageFunctions` (UiFunctionRuntimeDto: id, name, parameters,
|
|
22
|
+
* body, isAsync) — imza taşınır, GÖVDE TAŞINMAZ (MAX_RESPONSE_BYTES). Ajan Functions.<ad>(…)
|
|
23
|
+
* yazarken adı ve parametre sırasını buradan alır (G-06).
|
|
24
|
+
*/
|
|
25
|
+
function runtimeFunctions(value) {
|
|
26
|
+
return (Array.isArray(value) ? value : [])
|
|
27
|
+
.filter((entry) => Boolean(entry) && typeof entry === 'object')
|
|
28
|
+
.map(entry => ({
|
|
29
|
+
id: entry.id,
|
|
30
|
+
name: entry.name,
|
|
31
|
+
signature: functionSignature(entry),
|
|
32
|
+
isAsync: entry.isAsync === true,
|
|
33
|
+
parameters: (Array.isArray(entry.parameters) ? entry.parameters : [])
|
|
34
|
+
.filter((parameter) => Boolean(parameter) && typeof parameter === 'object')
|
|
35
|
+
.map(parameter => ({ name: parameter.name, required: parameter.required === true, defaultValue: parameter.defaultValue ?? null }))
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Yazma yanıtındaki şema gövdesinin biçimi (B-55 · S-9). Varsayılan `outline`: gövde ajanın
|
|
40
|
+
* aynı turda kendi gönderdiği içeriğin yankısıdır, kanıt ise pointer'da ve `silentlyDropped`ta
|
|
41
|
+
* durur. Bayrağı bilmeyen ajan pahalı olanı ödemesin diye kırpma OPT-OUT'tur (aynı karar:
|
|
42
|
+
* `DS-004`, `datasource_configuration_options`).
|
|
43
|
+
*/
|
|
44
|
+
const writeViewParam = z.enum(['outline', 'full']).optional().describe("Yanıttaki şema gövdesinin biçimi. outline (VARSAYILAN): komponent ağaçları iskelete iner (id/elementId/name/type/children), appliedPatch her op'un op+path'ini verir — 'yalnız o komponente mi dokunuldu' kanıtı JSON Pointer'dadır, 'kaydedildi mi' kanıtı silentlyDropped'tadır. full: platformun döndürdüğü gövde aynen gelir.");
|
|
45
|
+
/**
|
|
46
|
+
* `appliedPatch` zarfın altında da gelebilir (`{ data: { appliedPatch } }`), düz de.
|
|
47
|
+
*/
|
|
48
|
+
function outlineWriteResponse(response) {
|
|
49
|
+
const inner = response.data;
|
|
50
|
+
const container = (inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : response);
|
|
51
|
+
if (Array.isArray(container.appliedPatch))
|
|
52
|
+
container.appliedPatch = outlineAppliedPatch(container.appliedPatch);
|
|
53
|
+
}
|
|
54
|
+
const opsParam = z.array(z.record(z.unknown())).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} | set_binding {target, prop, expression: '{{ Page.variables.x }}'} | add_component {parent (üst elementId ya da 'root'), index?, component} | remove_component {target} | move_component {target, parent, index?, position: {x, y}} | set_event {target, event, eventValue} | remove_event {target, event} (gateway emülasyonu: platformun micro-op'u olay SİLEMEZ, eventValue: null yalnız değersiz ölü kayıt bırakır — bu op şemayı okuyup olayı diziden çıkarır ve sayfayı tam gövdeyle geri yazar) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler) | resize_component {target, size?: {w,h}, appearance?: {widthType,widthString,heightType,heightString}, states?: {isWrapper,isScrollable,isSticky,isDroppable,isFloating,isQuickStack}, breakpoint?: 1024} (gateway emülasyonu: platformun micro-op'unda boyut YOKTUR ve move_component'e konan size sessizce düşer — bu op şemayı okuyup hedefin boyutunu değiştirir ve sayfayı tam gövdeyle geri yazar. Izgarada blocks[].size, flex'te appearance geçerlidir; ebeveynin düzeni yazılanı etkisiz kılıyorsa PAGE-019 uyarısı çıkar. states bir PROP DEĞİL düğüm alanıdır ve replace_prop onu yazamaz — kurulmuş bir düğümün isWrapper bayrağını değiştirmenin tek yolu buydu). set_prop_type {target, prop, propType: 'static'|'code'|'translation', ref?, data?} (gateway emülasyonu: replace_prop gönderilen ham değeri HER ZAMAN static Prop'una sarar ve sarmalanmış Prop nesnesini reddeder — kurulu bir etiketi çeviriye bağlamanın tek yolu düğümü remove_component + add_component ile yeniden kurmaktı ve o yol düğüme yazan setter iteratee'lerini sessizce öldürüyordu. Bu op şemayı okuyup prop'un TİPİNİ yerinde değiştirir ve sayfayı tam gövdeyle geri yazar. Taşıyıcı alan tipe göre: static → data, code ve translation → ref (runtime data'yı her turda ezer, anahtar yalnız ref'te yaşar). Eksik taşıyıcı istek gönderilmeden reddedilir; page_schema_validate aynı kararı verir). target = elementId.");
|
|
13
55
|
/**
|
|
14
56
|
* PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
|
|
15
57
|
* komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
|
|
@@ -37,17 +79,6 @@ function collectTypes(nodes, into) {
|
|
|
37
79
|
collectTypes(node.children, into);
|
|
38
80
|
}
|
|
39
81
|
}
|
|
40
|
-
function invalidComponentNames(nodes) {
|
|
41
|
-
const found = [];
|
|
42
|
-
for (const node of nodes ?? []) {
|
|
43
|
-
const name = node.name;
|
|
44
|
-
if (typeof name === 'string' && name.length > 0 && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name.replace(/-/g, ''))) {
|
|
45
|
-
found.push(`'${name}' (${node.elementId ?? '?'})`);
|
|
46
|
-
}
|
|
47
|
-
found.push(...invalidComponentNames(node.children));
|
|
48
|
-
}
|
|
49
|
-
return found;
|
|
50
|
-
}
|
|
51
82
|
function removeNodeFromTree(nodes, target) {
|
|
52
83
|
for (let index = 0; index < nodes.length; index++) {
|
|
53
84
|
if (nodes[index].elementId === target)
|
|
@@ -157,8 +188,113 @@ async function applyEventRemovals(platform, project, environment, applicationId,
|
|
|
157
188
|
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
158
189
|
return removed;
|
|
159
190
|
}
|
|
160
|
-
|
|
191
|
+
function findParentNode(nodes, target) {
|
|
192
|
+
for (const node of nodes) {
|
|
193
|
+
if ((node.children ?? []).some(child => child.elementId === target))
|
|
194
|
+
return node;
|
|
195
|
+
const deeper = findParentNode(node.children ?? [], target);
|
|
196
|
+
if (deeper)
|
|
197
|
+
return deeper;
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* `resize_component` gateway emülasyonudur: platformun micro-op sözlüğünde boyut YOKTUR
|
|
203
|
+
* (`move_component` yalnız `position` uygular, gövdeye konan `size` 200 döner ve sessizce düşer —
|
|
204
|
+
* ölçüldü 09-04). Bu yüzden kurulmuş bir sayfada bir kartın yüksekliğini değiştirmenin tek yolu
|
|
205
|
+
* düğümü `remove_component` + `add_component` ile yeniden kurmaktı; o da ona yazan akış
|
|
206
|
+
* setter'larını sessizce öldürüyordu (FLOW-018). Op şemayı okur, hedefin boyutunu değiştirir ve
|
|
207
|
+
* sayfayı tam gövdeyle geri yazar — `remove_event` ile aynı desen.
|
|
208
|
+
*
|
|
209
|
+
* Izgarada boyut `blocks[].size`, flex'te `appearance` alanlarındadır; ikisi de kabul edilir ve
|
|
210
|
+
* ebeveynin düzeni okunanı etkisiz kılıyorsa uyarı üretilir (PAGE-015 ile aynı yaklaşım: reddetme,
|
|
211
|
+
* ama sessiz de kalma).
|
|
212
|
+
*/
|
|
213
|
+
/**
|
|
214
|
+
* G-25 — `states` bir prop değil düğüm alanı: `replace_prop` yazamıyor, `move_component` yalnız
|
|
215
|
+
* konum uyguluyor ve `resize_component` (G-14) boyutla sınırlıydı. Kurulmuş bir düğümün
|
|
216
|
+
* `isWrapper` bayrağını değiştirmenin tek yolu düğümü silip yeniden kurmaktı; 09-05 koşusunda
|
|
217
|
+
* bunun bedeli 20 setter'ın sessizce kopmasıydı (G-21). Anahtar kümesi beyaz listeyle sınırlı:
|
|
218
|
+
* `states` açık uçlu bir sözlük ve uydurma bayrak sessizce kaydedilirdi.
|
|
219
|
+
*/
|
|
220
|
+
const WRITABLE_STATES = new Set(['isWrapper', 'isScrollable', 'isSticky', 'isDroppable', 'isFloating', 'isQuickStack']);
|
|
221
|
+
async function applyResizes(platform, project, environment, applicationId, pageId, page, ops) {
|
|
222
|
+
const schema = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
223
|
+
const applied = [];
|
|
224
|
+
const warnings = [];
|
|
225
|
+
for (const op of ops) {
|
|
226
|
+
const target = String(op.target ?? '');
|
|
227
|
+
const size = op.size && typeof op.size === 'object' ? op.size : undefined;
|
|
228
|
+
const appearance = op.appearance && typeof op.appearance === 'object' ? op.appearance : undefined;
|
|
229
|
+
const states = op.states && typeof op.states === 'object' ? op.states : undefined;
|
|
230
|
+
const breakpoint = Number(op.breakpoint ?? 1024);
|
|
231
|
+
if (!target)
|
|
232
|
+
throw new PlatformApiError("resize_component için 'target' zorunludur.", 400);
|
|
233
|
+
if (!size && !appearance && !states) {
|
|
234
|
+
throw new PlatformApiError(`resize_component '${target}' için 'size' ({ w, h } — ızgara), 'appearance' ({ widthType, widthString, heightType, heightString } — flex) ya da 'states' ({ isWrapper, isScrollable, isSticky, isDroppable, isFloating, isQuickStack }) verilmelidir; üçü de boş.`, 400);
|
|
235
|
+
}
|
|
236
|
+
if (states) {
|
|
237
|
+
const unknownStates = Object.keys(states).filter(key => !WRITABLE_STATES.has(key));
|
|
238
|
+
if (unknownStates.length > 0) {
|
|
239
|
+
throw new PlatformApiError(`resize_component '${target}': bilinmeyen states anahtarı (${unknownStates.join(', ')}). Yazılabilir bayraklar: ${[...WRITABLE_STATES].join(', ')}.`, 400);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const node = findNode(schema, target);
|
|
243
|
+
if (!node)
|
|
244
|
+
throw new PlatformApiError(`resize_component hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
245
|
+
const parent = findParentNode(schema, target);
|
|
246
|
+
const inFlex = parent !== undefined && isFlexContainer(parent);
|
|
247
|
+
if (size) {
|
|
248
|
+
const blocks = Array.isArray(node.blocks) ? node.blocks : [];
|
|
249
|
+
const block = blocks.find(entry => Number(entry?.breakpoint) === breakpoint);
|
|
250
|
+
if (!block) {
|
|
251
|
+
throw new PlatformApiError(`resize_component '${target}' düğümünde ${breakpoint} breakpoint'li blok yok (var olanlar: ${blocks.map(entry => entry?.breakpoint).join(', ') || 'hiç'}). Farklı bir breakpoint verin ya da düğümü add_component ile blokları kurulu olarak ekleyin.`, 400);
|
|
252
|
+
}
|
|
253
|
+
const current = (block.size ?? {});
|
|
254
|
+
block.size = { w: Number(size.w ?? current.w ?? 0), h: Number(size.h ?? current.h ?? 0) };
|
|
255
|
+
applied.push(`${target}.size=${JSON.stringify(block.size)}`);
|
|
256
|
+
if (inFlex) {
|
|
257
|
+
warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex kapsayıcı; orada yerleşimi 'blocks[].size' DEĞİL appearance (widthType/widthString/heightType/heightString) belirler. Boyut yazıldı ama ekranda etkisi olmayabilir — flex'te ölçü vermek için aynı op'a 'appearance' koyun.`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (states) {
|
|
261
|
+
node.states = { ...(node.states ?? {}), ...states };
|
|
262
|
+
applied.push(`${target}.states=${JSON.stringify(states)}`);
|
|
263
|
+
if (states.isWrapper === true) {
|
|
264
|
+
warnings.push(`PAGE-020 (uyarı): '${target}' düğümüne isWrapper:true yazıldı — runtime sarmalayıcı düğüme 'min-height: 80vh' veriyor ve bu, sayfanın altına yüzlerce piksel ölü boşluk ekleyebilir (ölçüldü 09-05: dokuz düğüm ~1600px). style ile ezilemez; gerekmiyorsa isWrapper:false bırakın.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (appearance) {
|
|
268
|
+
node.appearance = { ...(node.appearance ?? {}), ...appearance };
|
|
269
|
+
applied.push(`${target}.appearance=${JSON.stringify(appearance)}`);
|
|
270
|
+
if (!inFlex && parent !== undefined) {
|
|
271
|
+
warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex DEĞİL; ızgarada yerleşimi 'blocks[].size' belirler ve appearance genişlik/yükseklik alanları etkisiz kalabilir. Izgarada ölçü vermek için aynı op'a 'size' koyun.`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (applied.length === 0)
|
|
276
|
+
return { applied, warnings };
|
|
277
|
+
const body = pageUpdateBody(page, { schema });
|
|
278
|
+
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
279
|
+
return { applied, warnings };
|
|
280
|
+
}
|
|
281
|
+
async function applyPropTypes(platform, project, environment, applicationId, pageId, page, ops) {
|
|
282
|
+
const schema = JSON.parse(JSON.stringify(page.schema ?? []));
|
|
283
|
+
const { applied, missingTargets } = applyPropTypesToSchema(schema, ops);
|
|
284
|
+
if (missingTargets.length > 0) {
|
|
285
|
+
throw new PlatformApiError(`set_prop_type hedefi bulunamadı: '${missingTargets[0]}'. page_schema_get ile elementId'leri listeleyin.`, 400);
|
|
286
|
+
}
|
|
287
|
+
if (applied.length === 0)
|
|
288
|
+
return [];
|
|
289
|
+
const body = pageUpdateBody(page, { schema });
|
|
290
|
+
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
291
|
+
return applied;
|
|
292
|
+
}
|
|
293
|
+
export function registerPageTools(server, platform, injectedScreenshotter) {
|
|
161
294
|
const config = platform.config;
|
|
295
|
+
const asUserParam = z.string().min(1).optional().describe("Uygulama üyesinin kimliği: istek gateway'in servis jetonu yerine bu kimlikle gider. Jeton HEM Authorization başlığında HEM NG_AccessToken çerezinde taşınır, böylece çerez okuyan middleware de geçer. İKİ ŞEKİL kabul edilir: (1) ham JWT, (2) `tok_…` referansı — uygulamanın giriş ucunu microservice_execute ile çağırdığınızda jeton maskelenir ama yanıtın `tokenRefs` alanında referansı döner; jetonu hiç görmeden onu buraya verin (15 dk geçerli). anonymous ile birlikte verilemez.");
|
|
296
|
+
/** Sayfa ve uç araçları aynı kimlik sözleşmesini paylaşır (G-20); çözüm tokenVault'ta yaşar. */
|
|
297
|
+
const resolveIdentity = resolveIdentityToken;
|
|
162
298
|
server.registerTool('page_list', {
|
|
163
299
|
title: 'Sayfaları listele',
|
|
164
300
|
description: 'Uygulamanın sayfalarını (id, name, slug) listeler; hedef sayfayı bulmak için ilk adım.',
|
|
@@ -192,6 +328,7 @@ export function registerPageTools(server, platform) {
|
|
|
192
328
|
name: page.name,
|
|
193
329
|
slug: page.slug,
|
|
194
330
|
browserTitle: page.browserTitle ?? '',
|
|
331
|
+
browserTitleKey: page.browserTitleKey ?? '',
|
|
195
332
|
isHomepage: page.isHomepage === true,
|
|
196
333
|
layoutId: page.layoutId ?? layout.id ?? null,
|
|
197
334
|
layoutName: layout.name,
|
|
@@ -208,7 +345,7 @@ export function registerPageTools(server, platform) {
|
|
|
208
345
|
}));
|
|
209
346
|
server.registerTool('page_runtime_get', {
|
|
210
347
|
title: 'Sayfanın runtime çıktısını getir',
|
|
211
|
-
description: "Çıkan uygulamanın sayfa için aldığı GERÇEK paketi döner (GET /{project}/{application}/p/{slug} — sayfanın ham serve yolu budur; Studio preview ve runtime aynı ucu tüketir): sayfa şeması + layout views'ü + sayfa olayları + flow'lar + değişken
|
|
348
|
+
description: "Çıkan uygulamanın sayfa için aldığı GERÇEK paketi döner (GET /{project}/{application}/p/{slug} — sayfanın ham serve yolu budur; Studio preview ve runtime aynı ucu tüketir): sayfa şeması + layout views'ü + sayfa olayları + flow'lar + değişken anahtarları (variableKeys: middleware değişkenleri, pageVariableKeys/appVariableKeys: state.page/state.app anahtarları) + kullanıcı fonksiyonu imzaları (appFunctions/pageFunctions — bir ifadeye Functions.<ad>(…) yazmadan önce adı ve parametre sırasını buradan alın; gövde taşınmaz, function_get ile okunur). Kurduğun sayfanın SON hâlini doğrulamanın yolu budur (layout-slot yerleşimi dahil — Kural 1f). DİKKAT: sayfaya bağlı middleware'ler bu çağrıda GERÇEKTEN koşar; middleware reddederse yanıt middlewareBlocked ile döner — korumalı sayfada bu beklenen davranıştır ve kendisi bir doğrulamadır.",
|
|
212
349
|
inputSchema: {
|
|
213
350
|
project: projectParam,
|
|
214
351
|
environment: environmentParam,
|
|
@@ -216,13 +353,20 @@ export function registerPageTools(server, platform) {
|
|
|
216
353
|
pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
|
|
217
354
|
view: z.enum(['summary', 'full']).optional().describe('summary (varsayılan): şema/layout ağaçları id/name/type özetine indirgenir. full: ham gövde — büyük olabilir'),
|
|
218
355
|
resolveStyles: z.boolean().optional().describe("summary görünümünde her düğüme EFEKTİF stilini ekler: tema componentStyles → preset → düğümün kendi stili kaskadı çözülür (aibox'ta tipli stil grupları da derlenir ve TABAN katman olur). styleLayers hangi katmanın konuştuğunu söyler — 'bu kutu neden böyle görünüyor' sorusunun cevabı budur."),
|
|
219
|
-
anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı olmadan gider — 'korumalı sayfa tokensız ne döner, şema sızıyor mu?' sorusunun cevabı budur. Korumalı sayfada middlewareBlocked bekleyin.")
|
|
356
|
+
anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı ve çerezi olmadan gider — 'korumalı sayfa tokensız ne döner, şema sızıyor mu?' sorusunun cevabı budur. Korumalı sayfada middlewareBlocked bekleyin."),
|
|
357
|
+
asUser: asUserParam
|
|
220
358
|
},
|
|
221
359
|
annotations: readOnly
|
|
222
|
-
}, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous }) => {
|
|
360
|
+
}, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous, asUser }) => {
|
|
223
361
|
let response;
|
|
362
|
+
if (anonymous === true && asUser !== undefined) {
|
|
363
|
+
throw new PlatformApiError('page_runtime_get: anonymous:true ile asUser birlikte verilemez — biri kimliksiz isteği, diğeri belirli bir üyenin kimliğiyle atılan isteği ister.');
|
|
364
|
+
}
|
|
365
|
+
// page_screenshot çerez, bu tool başlık taşıyordu; çerez okuyan middleware ikincisinden
|
|
366
|
+
// yapısal olarak geçemiyordu. Kimlik artık iki kanalda birden gider (Y-02).
|
|
367
|
+
const identity = asUser === undefined ? {} : { headers: { authorization: `Bearer ${resolveIdentity(asUser)}` } };
|
|
224
368
|
try {
|
|
225
|
-
response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous });
|
|
369
|
+
response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous, mirrorAuthorizationCookie: true, ...identity });
|
|
226
370
|
}
|
|
227
371
|
catch (error) {
|
|
228
372
|
if (error instanceof PlatformApiError && typeof error.status === 'number' && error.status >= 300) {
|
|
@@ -245,24 +389,28 @@ export function registerPageTools(server, platform) {
|
|
|
245
389
|
const asNodes = (value) => (Array.isArray(value) ? value : []);
|
|
246
390
|
const named = (value) => (Array.isArray(value) ? value : []).map(entry => ({ id: entry.id, name: entry.name, slug: entry.slug }));
|
|
247
391
|
return {
|
|
248
|
-
page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, isHomepage: body.isHomepage },
|
|
392
|
+
page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, browserTitleKey: body.browserTitleKey, isHomepage: body.isHomepage },
|
|
249
393
|
events: body.events ?? [],
|
|
250
394
|
layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(node => summarize(node, styleSource)) } : null,
|
|
251
395
|
schema: asNodes(body.schema).map(node => summarize(node, styleSource)),
|
|
252
396
|
widgets: named(body.widgets),
|
|
253
397
|
flows: named(body.flows),
|
|
254
398
|
variableKeys: Object.keys((body.variables ?? {})),
|
|
399
|
+
pageVariableKeys: parseVariableEntries(body.pageVariables).map(entry => entry.key),
|
|
400
|
+
appVariableKeys: parseVariableEntries(body.appVariables).map(entry => entry.key),
|
|
401
|
+
appFunctions: runtimeFunctions(body.appFunctions),
|
|
402
|
+
pageFunctions: runtimeFunctions(body.pageFunctions),
|
|
255
403
|
hasTheme: Boolean(body.project?.theme),
|
|
256
404
|
note: resolveStyles
|
|
257
405
|
? "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f). resolvedStyle = componentStyles → preset → düğüm stili kaskadı (aibox'ta tipli grupların derlemesi TABAN katmandır); styleLayers hangi katmanların konuştuğunu söyler."
|
|
258
406
|
: "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f); tam gövde gerekirse view:'full'. Bir düğümün ekranda GERÇEKTEN aldığı stili görmek için resolveStyles:true."
|
|
259
407
|
};
|
|
260
408
|
}));
|
|
261
|
-
const screenshotter = new StudioScreenshotter(config);
|
|
409
|
+
const screenshotter = injectedScreenshotter ?? new StudioScreenshotter(config);
|
|
262
410
|
const maxInlinePngBytes = 5 * 1024 * 1024;
|
|
263
411
|
server.registerTool('page_screenshot', {
|
|
264
412
|
title: 'Sayfanın ekran görüntüsünü al',
|
|
265
|
-
description: "Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve hata
|
|
413
|
+
description: "Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve Studio'nun çizdiği hata durumu karesi döner — bu da bir doğrulamadır. YÜKLENMEMİŞ SAYFA ASLA ÇEKİLMEZ: kare, runtime çizilip mount akışları bitince alınır (yanıtta pageState + readyAfterMs); bütçe dolduğunda sayfa hâlâ iskelette ya da akış koşuyorsa çağrı ne görüldüğünü söyleyen bir hatayla döner, boş/yarım kare dönmez. Sayfa akışı bir adımda düşerse (patlayan ifade, hata dönen uç) yanıt runtimeErrors taşır ve hangi komponentin hangi prop'unun YAZILMADIĞINI söyler — kare 'çalışıyor' görünürken sessizce boş kalan komponenti ancak bu yakalar. Kare DİLİ ve TEMASI seçilebilir: 'language' adrese ?lang ekler (çeviriye bağlı prop'lar da çok dilli alanların satır değerleri de o dilde çözülür), 'colorScheme' ziyaretçinin tema tercihini sayfa açılmadan eker — ikisi de sayfaya dil değiştirici ya da theme-toggle koymayı GEREKTİRMEZ, yani referansa sadık bir tasarımın ikinci dili ve koyu teması bozulmadan kanıtlanır. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir. DAL ORTAMI DA GÖRÜNTÜLENİR (2026-09-09'dan beri): 'development' /dev, 'production' /view, dal ise /branch/{dal} şeridinden açılır ve sayfanın BÜTÜN istekleri (şema da akışlar da) o dala gider — dalda kurulan sayfa artık merge'i beklemeden gözle doğrulanır (platform_guide('merge') §Dalda görsel doğrulama).",
|
|
266
414
|
inputSchema: {
|
|
267
415
|
project: projectParam,
|
|
268
416
|
environment: environmentParam,
|
|
@@ -273,50 +421,161 @@ export function registerPageTools(server, platform) {
|
|
|
273
421
|
height: z.number().int().min(240).max(2160)
|
|
274
422
|
}).optional().describe('Varsayılan 1280x800 (deviceScaleFactor 1)'),
|
|
275
423
|
fullPage: z.boolean().optional().describe('true: kaydırılan kısım dahil tüm sayfa; varsayılan yalnız viewport karesi'),
|
|
276
|
-
delayMs: z.number().int().min(0).max(
|
|
277
|
-
|
|
424
|
+
delayMs: z.number().int().min(0).max(60000).optional().describe("Hazırlık bütçesine EK süre (ms). Varsayılan bütçe 15 sn'dir ve kare sayfa hazır olur olmaz alınır (akışsız giriş ekranı ~1 sn, dashboard mount akışları bitince) — normalde vermeye gerek yok. Yalnız sayfa gerçekten uzun sürüyorsa (soğuk Vite derlemesi, ağır mount akışı) büyütün: bütçe dolduğunda sayfa hâlâ hazır değilse çağrı hatayla döner, kare alınmaz. Sayfanın AÇILMASI için beklenen 20 sn'lik navigasyon süresi de bu kadar uzar — soğuk Studio'da tek düğme budur (09-08 S-8)."),
|
|
425
|
+
captureUnready: z.boolean().optional().describe("true: TEŞHİS kipi — bütçe dolduğunda sayfa hazır olmasa da kare alınır ve yanıt pageState: 'not-ready' taşır (iskelet/yarım kare görülebilir). Varsayılan false: yüklenmemiş sayfa çekilmez, hata döner."),
|
|
426
|
+
anonymous: z.boolean().optional().describe("true: hiçbir çerez enjekte edilmez — anonim ziyaretçinin gördüğü kare. Korumalı sayfada middlewareBlocked bekleyin."),
|
|
427
|
+
asUser: asUserParam,
|
|
428
|
+
expectFailure: z.boolean().optional().describe("true: bu çekimde bir akış adımının düşmesi BEKLENİYOR (kasıtlı negatif test — yanlış parola, yetkisiz kullanıcı, geçersiz gövde). Kare yine alınır ve runtimeErrors yine dolar, ama yanıt expectedFailure:true taşır ve benchmark raporu bu satırı plansız hata saymaz: 2026-09-06 koşusunda on başarısız çağrının ALTISI kasıtlı negatif testti ve ayıklama elle yapıldı (S-4)."),
|
|
429
|
+
layoutReport: z.boolean().optional().describe("true: kareyle birlikte her runtime düğümünün efektif piksel kutusunu ve özetini (taşan / viewport dışı / sıfır boyutlu elementId'ler) döner — taşma ve çakışmayı görüntüsüz, sayısal yakalar. Düğümler data-element-id taşımıyorsa layoutReportUnavailable döner."),
|
|
430
|
+
cookies: z.record(z.string()).optional().describe("Tarayıcıya enjekte edilecek EK çerezler (ad → değer): uygulamanın kendi oturum çerezi buraya verilir. Aynı ada sahip çerez gateway jetonunun yerine geçer. anonymous ile birlikte verilemez."),
|
|
431
|
+
actions: z.array(z.object({
|
|
432
|
+
action: z.enum(['set', 'click', 'wait']),
|
|
433
|
+
target: z.string().min(1).optional().describe("Hedefin elementId'si (page_schema_get); runtime düğümü data-element-id ile bulunur"),
|
|
434
|
+
value: z.unknown().optional().describe("'set' için yazılacak değer"),
|
|
435
|
+
ms: z.number().int().min(0).max(15000).optional().describe("'wait' için bekleme süresi")
|
|
436
|
+
})).max(20).optional().describe("Kare ALINMADAN ÖNCE sayfada koşturulacak etkileşim adımları: [{action:'set', target:'flt_month', value:'2022-10'}, {action:'click', target:'btn_sign_in'}]. 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez). Her adımdan sonra yerleşme beklenir ve yanıttaki actionsReport hedefin bulunup bulunmadığını ve adımdan sonra KAÇ AĞ İSTEĞİ çıktığını söyler — 'bağ kuruldu' ile 'çalışıyor' arasındaki farkı ancak bu ayırır (G-24)."),
|
|
437
|
+
language: z.string().min(1).optional().describe("Kareyi BU DİLDE çeker: adrese '?lang=<kod>' eklenir ve runtime dili oradan seçer (öncelik: ?lang → tarayıcıda kayıtlı seçim → tarayıcı dilleri → projenin varsayılanı). Seçim hem çeviriye bağlı prop'ları hem çok dilli alanların satır değerlerini kapsar, yani ikinci dilin karesi için sayfaya dil değiştirici KOYMAK GEREKMEZ — referans tasarımı bozulmadan ikinci dil kanıtlanır. ISO 639-1 iki harfli kod ('tr', 'en'); bölgeli kod ('tr-TR') indirgenir ve indirgeme yanıtta bildirilir. Kod projenin languages listesinde yoksa çağrı REDDEDİLİR: runtime tanımadığı kodu yok sayıp kareyi varsayılan dilde çizerdi ve 'çevrildi' sanılan yanlış bir kare raporlanırdı."),
|
|
438
|
+
colorScheme: z.enum(['light', 'dark']).optional().describe("Kareyi açık ya da koyu temada çeker. Ziyaretçi tema tercihi çerezde değil localStorage'da yaşar ve tema store'u onu sayfa açılırken bir kez okur; gateway tercihi sayfa script'lerinden ÖNCE eker, bu yüzden sayfada 'theme-toggle' komponenti OLMASA DA koyu tema karesi alınabilir. Verilmezse projenin kendi modu çizilir."),
|
|
278
439
|
outPath: z.string().min(1).optional().describe('Verilirse PNG bu yerel yola yazılır ve inline görüntü DÖNMEZ; yalnız özet (yol, boyut) döner — büyük fullPage kareleri için')
|
|
279
440
|
},
|
|
280
441
|
annotations: readOnly
|
|
281
|
-
}, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, anonymous, outPath }) => {
|
|
442
|
+
}, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, captureUnready, anonymous, asUser, cookies, layoutReport, actions, outPath, expectFailure, language, colorScheme }) => {
|
|
282
443
|
try {
|
|
444
|
+
if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
|
|
445
|
+
throw new PlatformApiError("page_screenshot: anonymous:true ile asUser/cookies birlikte verilemez — biri kimliksiz kareyi, diğeri belirli bir kimlikle çekilen kareyi ister. Anonim kare için yalnız anonymous:true, üye kimliğiyle kare için yalnız asUser (ve gerekiyorsa cookies) gönderin.");
|
|
446
|
+
}
|
|
283
447
|
if (!config.studioBaseUrl) {
|
|
284
448
|
throw new PlatformApiError("page_screenshot için STUDIO_BASE_URL tanımlı değil. Studio arayüzünün kök adresini .env'e ekleyin (yerel geliştirmede http://localhost:3000, dağıtımda nginx adresi).");
|
|
285
449
|
}
|
|
286
450
|
const resolvedEnvironment = platform.resolveEnvironment(environment);
|
|
287
|
-
|
|
451
|
+
// U-04 (2026-09-09): kısıt önce gateway'de DEĞİL Studio'da olduğu için reddediliyordu —
|
|
452
|
+
// runtime rotası dal adı taşımıyor, ortamı modül içi bir store belirliyordu ve dal adıyla
|
|
453
|
+
// istenen kare development'ı gösterirdi. Studio artık `/branch/{env}/…` şeridini tanıyor ve
|
|
454
|
+
// ortamı URL'den okuyor (f-02); dal karesi bu yüzden GERÇEKTEN o dalın karesi. Geriye yalnız
|
|
455
|
+
// platformun dal diye kabul etmeyeceği adlar kalıyor — onlar için şerit kurulmaz.
|
|
456
|
+
const lane = resolvedEnvironment === 'development'
|
|
457
|
+
? 'dev'
|
|
458
|
+
: resolvedEnvironment === 'production'
|
|
459
|
+
? 'view'
|
|
460
|
+
: isBranchName(resolvedEnvironment) ? `branch/${resolvedEnvironment}` : undefined;
|
|
288
461
|
if (!lane) {
|
|
289
|
-
throw new PlatformApiError(`page_screenshot
|
|
462
|
+
throw new PlatformApiError(`page_screenshot '${resolvedEnvironment}' ortamını görüntüleyemez: bu ad ne sabit bir ortam ne de geçerli bir dal adı. Sabitler 'development' (/dev) ve 'production' (/view); dal adı bir harfle başlar, yalnız küçük harf/rakam/alt çizgi taşır (3-31 karakter) ve rezerve olamaz ('public', 'api', 'pg_*' gibi). Projedeki dalları environment_list ile görün.`);
|
|
463
|
+
}
|
|
464
|
+
const projectSlug = platform.resolveProject(project);
|
|
465
|
+
const languageChoice = language === undefined ? undefined : normalizeLanguageCode(language, 'language');
|
|
466
|
+
if (languageChoice) {
|
|
467
|
+
// Bağ burada kurulmazsa sessiz yanlış kare doğar: runtime tanımadığı kodu yok sayar ve
|
|
468
|
+
// sayfayı varsayılan dilde çizer (resolveRuntimeLanguage yalnız projenin taşıdığı kodu
|
|
469
|
+
// seçer), kare de 'ikinci dil' diye rapora girerdi.
|
|
470
|
+
const state = readProjectLanguages(await platform.org('GET', `/p/${projectSlug}`).catch(() => undefined));
|
|
471
|
+
if (state && state.languages.length > 0 && !state.languages.includes(languageChoice.code)) {
|
|
472
|
+
throw new PlatformApiError(`page_screenshot: '${languageChoice.code}' projenin dilleri arasında değil (${state.languages.join(', ')}). Runtime bilmediği kodu yok sayıp kareyi varsayılan dilde çizer — dili project_update(languages) ile ekleyin ya da listedeki bir kodu verin.`);
|
|
473
|
+
}
|
|
290
474
|
}
|
|
291
|
-
const url = `${config.studioBaseUrl}/${lane}/${
|
|
475
|
+
const url = `${config.studioBaseUrl}/${lane}/${projectSlug}/${applicationSlug}/${pageSlug}${languageChoice ? `?lang=${languageChoice.code}` : ''}`;
|
|
292
476
|
const size = viewport ?? { width: 1280, height: 800 };
|
|
477
|
+
const extraCookies = Object.entries(cookies ?? {}).map(([name, value]) => ({ name, value }));
|
|
478
|
+
const identity = anonymous === true ? 'anonymous' : asUser === undefined ? 'gateway' : 'asUser';
|
|
479
|
+
const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
|
|
293
480
|
const shot = await screenshotter.capture({
|
|
294
481
|
url,
|
|
295
482
|
apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
|
|
296
|
-
token: anonymous ? undefined : await platform.accessToken(),
|
|
483
|
+
token: anonymous ? undefined : resolveIdentity(asUser) ?? await platform.accessToken(),
|
|
484
|
+
...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
|
|
297
485
|
cookieUrl: config.studioBaseUrl,
|
|
298
486
|
viewport: size,
|
|
299
487
|
fullPage: fullPage === true,
|
|
300
|
-
delayMs: delayMs ?? 0
|
|
488
|
+
delayMs: delayMs ?? 0,
|
|
489
|
+
...(captureUnready === true ? { captureUnready: true } : {}),
|
|
490
|
+
...(layoutReport === true ? { layoutReport: true } : {}),
|
|
491
|
+
...(colorScheme === undefined ? {} : { colorScheme }),
|
|
492
|
+
...(actions === undefined ? {} : { actions: actions })
|
|
301
493
|
});
|
|
302
494
|
const meta = {
|
|
303
495
|
url,
|
|
304
496
|
environment: resolvedEnvironment,
|
|
497
|
+
...(languageChoice ? { language: languageChoice.code } : {}),
|
|
498
|
+
...(languageChoice?.note ? { languageNote: languageChoice.note } : {}),
|
|
499
|
+
...(colorScheme === undefined ? {} : { colorScheme }),
|
|
305
500
|
viewport: size,
|
|
306
501
|
fullPage: fullPage === true,
|
|
502
|
+
identity,
|
|
503
|
+
cookieNames,
|
|
307
504
|
sizeBytes: shot.png.byteLength,
|
|
308
505
|
elapsedMs: shot.elapsedMs,
|
|
506
|
+
...(shot.contentHeight === undefined ? {} : { contentHeight: shot.contentHeight }),
|
|
507
|
+
...(shot.fullPageTruncated
|
|
508
|
+
? { fullPageTruncated: true, note: `Sayfa içeriği ${shot.contentHeight} px — kare 16384 px'de kırpıldı. Alt kısmı görmek için viewport'u kaydırılmış bir bölüme odaklayın ya da sayfayı bölerek doğrulayın.` }
|
|
509
|
+
: {}),
|
|
309
510
|
...(shot.apiStatus === undefined ? {} : { apiStatus: shot.apiStatus }),
|
|
310
511
|
...(shot.apiStatus !== undefined && shot.apiStatus >= 300
|
|
311
|
-
? { middlewareBlocked: true, note:
|
|
512
|
+
? { middlewareBlocked: true, note: `Sayfanın veri isteği reddedildi — karede hata/boş durum görünüyor olabilir. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır (page_runtime_get ile karşılaştırın).${identity === 'gateway' ? " Gateway'in servis jetonu uygulamanın kendi kullanıcı tablosuna çözülmeyebilir; üye kimliğiyle çekmek için asUser (ve gerekiyorsa cookies) verin. Uygulamanın 'users' tablosu boşsa hiçbir kimlik çözülmez; yeni projede o tablo BOŞ gelir — row_list('users') ile bakın, ilk kullanıcıyı açma yolu platform_guide('getting-started') §'İlk kullanıcıyı kim açar?'." : ''}` }
|
|
513
|
+
: {}),
|
|
514
|
+
// P1-1 (S-3): eşzamanlı çekimler gateway içinde SERİ koşar; ajan yavaşlığın kuyruktan mı
|
|
515
|
+
// yoksa sayfadan mı geldiğini bilsin.
|
|
516
|
+
...(shot.queuedMs === undefined ? {} : { queuedMs: shot.queuedMs }),
|
|
517
|
+
...(shot.contextRecycled === true
|
|
518
|
+
? { contextRecycled: true, contextRecycledNote: 'Önceki çekim tarayıcı bağlamını bozdu; bu kare TEMİZ bir tarayıcıyla alındı — oturum çerezleri yeniden enjekte edildi.' }
|
|
519
|
+
: {}),
|
|
520
|
+
// S-8 (09-08): soğuk Studio'da navigasyon zaman aşımı ajanın çağrıyı tekrarlamasını
|
|
521
|
+
// gerektiriyordu; artık gateway bir kez kendi deniyor ve karenin neden geciktiğini söylüyor.
|
|
522
|
+
...(shot.navigationRetried === true
|
|
523
|
+
? { navigationRetried: true, navigationRetriedNote: 'İlk navigasyon zaman aşımına düştü ve kare ikinci denemede alındı — Studio soğuktu (ilk istek Vite derlemesini ısıtır). Sonraki çekimler hızlı olmalı; olmuyorsa delayMs verin, o değer navigasyon bütçesini de uzatır.' }
|
|
524
|
+
: {}),
|
|
525
|
+
// B-6 (2026-09-07): kare yalnız sayfa hazırken alınır; hangi kanıtla hazır sayıldığı yazılır.
|
|
526
|
+
pageState: shot.pageState,
|
|
527
|
+
readyAfterMs: shot.readyAfterMs,
|
|
528
|
+
...(shot.finalUrl === undefined ? {} : { finalUrl: shot.finalUrl }),
|
|
529
|
+
...(shot.pageState === 'ready-no-flows'
|
|
530
|
+
? { pageStateNote: 'Runtime çizildi ama hiç mount akışı koşmadı. Sayfa akışsızsa (giriş ekranı) normaldir; veri bekliyorsanız page_schema_get ile mount olayına bağlı akış olup olmadığına bakın.' }
|
|
531
|
+
: shot.pageState === 'ready-no-counter'
|
|
532
|
+
? { pageStateNote: 'Runtime koşan akış sayacı yayınlamıyor (eski runtime); hazırlık DOM durulmasından çıkarıldı, akışların bittiği kanıtlanamadı.' }
|
|
533
|
+
: shot.pageState === 'error'
|
|
534
|
+
? { pageStateNote: 'Studio sayfa için hata/bulunamadı durumunu çizdi; kare o durumu gösterir. apiStatus ve slug\'ları doğrulayın.' }
|
|
535
|
+
: shot.pageState === 'redirected'
|
|
536
|
+
? { pageStateNote: `İstenen adres runtime dışına yönlendi (finalUrl). Route guard ya da Studio giriş ekranı olabilir; kare yönlendirilen sayfayı gösterir.` }
|
|
537
|
+
: shot.pageState === 'not-ready'
|
|
538
|
+
? { pageStateNote: 'TEŞHİS KARESİ (captureUnready): bütçe dolduğunda sayfa hazır değildi; kare iskelet ya da yarım durumu gösterebilir, doğrulama için kullanmayın.' }
|
|
539
|
+
: {}),
|
|
540
|
+
...(shot.renderWaitTimedOut && shot.pageState === 'not-ready'
|
|
541
|
+
? { renderWaitTimedOut: true, note: "Sayfanın veri isteği gözlenmedi — STUDIO_BASE_URL ve slug'ları doğrulayın." }
|
|
312
542
|
: {}),
|
|
313
|
-
...(shot.
|
|
314
|
-
? {
|
|
315
|
-
:
|
|
543
|
+
...(shot.flowsSettled === undefined
|
|
544
|
+
? {}
|
|
545
|
+
: shot.flowsSettled
|
|
546
|
+
? { flowsSettled: true }
|
|
547
|
+
: { flowsSettled: false, flowsSettledNote: 'Sayfanın akışları bütçe içinde bitmedi; kare akış koşarken alındı (captureUnready). Akış gerçekten uzunsa delayMs ile bütçeyi büyütün.' }),
|
|
548
|
+
...(shot.actions === undefined ? {} : { actionsReport: shot.actions }),
|
|
549
|
+
// S-4: kasıtlı negatif test de akış hatası bırakır ve ölçüm aracı onu plansız hata
|
|
550
|
+
// sayıyordu. Çağıran beklediğini söylediğinde satır ayrı sayılır.
|
|
551
|
+
...(expectFailure === true ? { expectedFailure: true } : {}),
|
|
552
|
+
...(shot.runtimeErrors === undefined
|
|
553
|
+
? {}
|
|
554
|
+
: {
|
|
555
|
+
runtimeErrors: shot.runtimeErrors,
|
|
556
|
+
runtimeErrorsNote: `Sayfa akışı ${shot.runtimeErrors.length} yapısal hata bıraktı; kare 'çalışıyor' görünse bile bu adımlar işini YAPMADI. element/property alanı hangi komponentin hangi prop'unun yazılamadığını söyler — o komponent eski/boş değerindedir. Adımı flow_get ile açıp ifadeyi düzeltin.`
|
|
557
|
+
}),
|
|
558
|
+
...(shot.actions?.some(outcome => !outcome.found)
|
|
559
|
+
? { actionsNote: `Bulunamayan hedef var: ${shot.actions.filter(outcome => !outcome.found).map(outcome => outcome.target ?? '?').join(' · ')}. elementId'leri page_schema_get ile doğrulayın; runtime düğümü data-element-id ile bulunuyor.` }
|
|
560
|
+
: {}),
|
|
561
|
+
// 'wait' bir eylem değil bekleyiştir: akış sayacı ona bağlanmaz, notu tetiklemez (33/S-1).
|
|
562
|
+
...(shot.actions?.some(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0)
|
|
563
|
+
? { actionsNote: `Uygulandı ama HİÇ AĞ İSTEĞİ çıkmadı ve HİÇBİR AKIŞ KOŞMADI: ${shot.actions.filter(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0).map(outcome => `${outcome.action} '${outcome.target ?? '?'}'`).join(' · ')}. Olay bağı şemada görünse bile akış koşmamış olabilir — bağın gerçekten kurulu olduğunu page_runtime_get ile, akışın gövdesini flow_get ile doğrulayın.` }
|
|
564
|
+
: {}),
|
|
565
|
+
...(shot.layout === undefined
|
|
566
|
+
? {}
|
|
567
|
+
: shot.layout.length === 0
|
|
568
|
+
? { layoutReportUnavailable: true, note: "Sayfada data-element-id taşıyan düğüm yok — runtime bu attribute'u henüz basmıyor olabilir (F-04) ya da sayfa boş render edildi." }
|
|
569
|
+
: {
|
|
570
|
+
layoutSummary: summarizeLayout(shot.layout, size),
|
|
571
|
+
...(shot.layout.length <= 100 ? { layout: shot.layout } : { layoutTruncatedTo: 'summary' })
|
|
572
|
+
})
|
|
316
573
|
};
|
|
317
574
|
if (outPath) {
|
|
318
575
|
const absolute = resolve(outPath);
|
|
319
576
|
try {
|
|
577
|
+
// B-6: klasör yoksa ENOENT ile düşüyordu; koşu ajanı her seferinde klasörü elle açıyordu.
|
|
578
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
320
579
|
await writeFile(absolute, shot.png);
|
|
321
580
|
}
|
|
322
581
|
catch (error) {
|
|
@@ -376,10 +635,16 @@ export function registerPageTools(server, platform) {
|
|
|
376
635
|
server.registerTool('page_schema_validate', {
|
|
377
636
|
title: 'Sayfa şemasını doğrula (kuru çalıştırma)',
|
|
378
637
|
description: "Micro-op'ları kaydetmeden şemaya uygular ve doğrulama hatalarını (suggestions ile) döndürür; page_schema_patch öncesi planı test etmek için. Gövdeye page_schema_patch ile AYNI normalizasyonu uygular (Prop sarmalı, validation biçimi) — kuru koşu ile yazma aynı gövdeyi görür.",
|
|
379
|
-
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional() },
|
|
638
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional(), operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için") },
|
|
380
639
|
annotations: readOnly
|
|
381
|
-
}, handle(config, async ({ project, environment, applicationId, pageId, ops }) => {
|
|
382
|
-
const
|
|
640
|
+
}, handle(config, async ({ project, environment, applicationId, pageId, ops, operations }) => {
|
|
641
|
+
const allOps = microOps('page_schema_validate', ops, operations).map(op => op);
|
|
642
|
+
const propTypeIssues = setPropTypeErrors(allOps.filter(entry => entry.op === 'set_prop_type'));
|
|
643
|
+
if (propTypeIssues.length > 0)
|
|
644
|
+
throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
|
|
645
|
+
// Gateway emülasyonu olan op'lar platformun doğrulama ucunu tanımadığı için gövdeden ayrılır;
|
|
646
|
+
// kuru koşu ile yazma AYNI kararı vermeli (P0-2 Step 3).
|
|
647
|
+
const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
|
|
383
648
|
const normalized = normalizeSchemaOps(opRecords);
|
|
384
649
|
const validationIssues = schemaOpValidationErrors(opRecords);
|
|
385
650
|
const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
|
|
@@ -396,22 +661,34 @@ export function registerPageTools(server, platform) {
|
|
|
396
661
|
}));
|
|
397
662
|
server.registerTool('page_schema_patch', {
|
|
398
663
|
title: 'Sayfa şemasına patch uygula',
|
|
399
|
-
description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. expectedUpdatedAt verilirse sayfa o okumadan beri değişmişse istek reddedilir (stale plan koruması). Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler.",
|
|
664
|
+
description: "Micro-op'ları doğrulama duvarından geçirip kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. expectedUpdatedAt verilirse sayfa o okumadan beri değişmişse istek reddedilir (stale plan koruması). Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler; gövdesi VARSAYILAN OLARAK iskelettir (eklenen düğüm id/type/children'a iner) — tam gövde için view:'full'. Op'un gerçekten kaydedilip kaydedilmediğini gövde değil silentlyDropped söyler.",
|
|
400
665
|
inputSchema: {
|
|
401
666
|
project: projectParam,
|
|
402
667
|
environment: environmentParam,
|
|
403
668
|
applicationId: guid,
|
|
404
669
|
pageId: guid,
|
|
405
|
-
ops: opsParam,
|
|
406
|
-
|
|
670
|
+
ops: opsParam.optional(),
|
|
671
|
+
operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"),
|
|
672
|
+
expectedUpdatedAt: z.string().optional().describe("page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz"),
|
|
673
|
+
allowFlowDeletion: z.boolean().optional().describe("true: bu yama bir akışı referanssız bırakacaksa yine de uygulanır. Varsayılan false — platform referanssız akışı KALICI siler ve gateway isteği önden reddeder (G-21). Silinen akışlar yanıtta deletedFlows ile listelenir."),
|
|
674
|
+
view: writeViewParam
|
|
407
675
|
},
|
|
408
676
|
annotations: updates
|
|
409
|
-
}, handle(config, async ({ project, environment, applicationId, pageId, ops, expectedUpdatedAt }) => {
|
|
410
|
-
const allOps = ops.map(op => op);
|
|
677
|
+
}, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, expectedUpdatedAt, allowFlowDeletion, view }) => {
|
|
678
|
+
const allOps = microOps('page_schema_patch', ops, operations).map(op => op);
|
|
411
679
|
const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
|
|
412
|
-
const
|
|
680
|
+
const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
|
|
681
|
+
const propTypeOps = allOps.filter(entry => entry.op === 'set_prop_type');
|
|
682
|
+
const opRecords = allOps.filter(entry => !['remove_event', 'resize_component', 'set_prop_type'].includes(String(entry.op)));
|
|
683
|
+
const propTypeIssues = setPropTypeErrors(propTypeOps);
|
|
684
|
+
if (propTypeIssues.length > 0)
|
|
685
|
+
throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
|
|
413
686
|
const containerOps = opRecords
|
|
414
687
|
.filter(entry => (entry.op === 'remove_component' || entry.op === 'move_component') && entry.target === PAGE_CONTAINER_ELEMENT_ID);
|
|
688
|
+
const sizedMoves = opRecords.filter(entry => entry.op === 'move_component' && entry.size !== undefined);
|
|
689
|
+
if (sizedMoves.length > 0) {
|
|
690
|
+
throw new PlatformApiError(`move_component yalnız 'position' uygular; gövdeye konan 'size' platformda SESSİZCE düşer (ölçüldü 2026-09-04, appliedPatch'te yalnız /blocks/0/position görünür). Boyut için resize_component kullanın: { op: 'resize_component', target: '<elementId>', size: { w, h } } — flex kapsayıcı içindeyse 'appearance'. Etkilenen hedefler: ${sizedMoves.map(entry => entry.target).join(' · ')}`, 400);
|
|
691
|
+
}
|
|
415
692
|
if (containerOps.length > 0) {
|
|
416
693
|
throw new PlatformApiError(`'${PAGE_CONTAINER_ELEMENT_ID}' sayfanın kök kapsayıcısıdır; silinmez ve taşınmaz (PAGE-006) — silinirse sayfa iskeleti bozulur ve Studio komponentleri kapsayıcı dışında bırakır. Komponent kaldırmak için remove_component'i kapsayıcının İÇİNDEKİ elementId'lerle kullanın (page_schema_get ile listeleyin).`, 400);
|
|
417
694
|
}
|
|
@@ -455,23 +732,27 @@ export function registerPageTools(server, platform) {
|
|
|
455
732
|
if (unknown.length > 0)
|
|
456
733
|
throw new PlatformApiError(`PAGE-008 — geçersiz olay adı (istek gönderilmedi): ${unknown.join(' · ')}`, 400);
|
|
457
734
|
}
|
|
735
|
+
// Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
|
|
736
|
+
// silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
|
|
737
|
+
let deletedFlows = [];
|
|
738
|
+
if (opRecords.some(entry => ['remove_component', 'set_event', 'remove_event'].includes(String(entry.op)))) {
|
|
739
|
+
try {
|
|
740
|
+
const page = await readPage();
|
|
741
|
+
deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
deletedFlows = [];
|
|
745
|
+
}
|
|
746
|
+
if (deletedFlows.length > 0 && allowFlowDeletion !== true) {
|
|
747
|
+
throw new PlatformApiError(orphanedFlowMessage(deletedFlows), 400);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
458
750
|
const added = opRecords
|
|
459
751
|
.filter(entry => entry.op === 'add_component')
|
|
460
752
|
.map(entry => entry.component);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const badNames = invalidComponentNames(added);
|
|
465
|
-
if (badNames.length > 0) {
|
|
466
|
-
throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badNames.join(' · ')}. `
|
|
467
|
-
+ 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; boşluklu ya da geçersiz bir ad '
|
|
468
|
-
+ 'SAYFADAKİ TÜM ifadeleri "Missing initializer in const declaration" ile öldürür (layout adları her sayfaya sızar). '
|
|
469
|
-
+ 'name alanını elementId ile aynı yaz; okunabilir Türkçe başlık için props.title / props.text / props.label kullan.', 400);
|
|
470
|
-
}
|
|
471
|
-
const catalog = added.length > 0
|
|
472
|
-
? await applyCatalogLayoutRules(platform, project, environment, applicationId, added)
|
|
473
|
-
: { clamped: [], heights: [] };
|
|
474
|
-
const formWarnings = formBindingWarnings(opRecords);
|
|
753
|
+
assertScreenWriteGuards('page', { schema: added });
|
|
754
|
+
const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, added);
|
|
755
|
+
const formWarnings = [...formBindingWarnings(opRecords), ...formSubmitButtonWarnings(opRecords)];
|
|
475
756
|
let flowWarnings = [];
|
|
476
757
|
let layoutWarnings = [];
|
|
477
758
|
if (opRecords.some(entry => entry.op === 'remove_component')) {
|
|
@@ -482,6 +763,16 @@ export function registerPageTools(server, platform) {
|
|
|
482
763
|
catch {
|
|
483
764
|
flowWarnings = [];
|
|
484
765
|
}
|
|
766
|
+
// Ters yön (FLOW-018): silinen düğüme YAZAN setter'lar. Akış gövdeleri şemada değil, ayrı
|
|
767
|
+
// uçta; okunamazsa uyarı üretmeyiz ama yama yine geçer.
|
|
768
|
+
try {
|
|
769
|
+
const hosted = await platform.tenant('GET', project, flowPath('page', applicationId, pageId), { environment });
|
|
770
|
+
const page = await readPage();
|
|
771
|
+
flowWarnings = [...flowWarnings, ...staleSetterWarnings(hosted?.data ?? [], opRecords, page.schema)];
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
// akış listesi okunamadı
|
|
775
|
+
}
|
|
485
776
|
}
|
|
486
777
|
let nestedAfterOps = [];
|
|
487
778
|
const simulatable = opRecords.some(entry => ['add_component', 'move_component'].includes(String(entry.op))
|
|
@@ -498,7 +789,8 @@ export function registerPageTools(server, platform) {
|
|
|
498
789
|
...pageContainerPositionWarning(page.schema),
|
|
499
790
|
...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
|
|
500
791
|
...stringStyleWarning(stringStyleNodes(simulated)),
|
|
501
|
-
...inertFlexPropWarning(inertFlexPropNodes(simulated))
|
|
792
|
+
...inertFlexPropWarning(inertFlexPropNodes(simulated)),
|
|
793
|
+
...flexSizeWarnings(simulated, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')
|
|
502
794
|
];
|
|
503
795
|
}
|
|
504
796
|
catch {
|
|
@@ -512,29 +804,62 @@ export function registerPageTools(server, platform) {
|
|
|
512
804
|
: [];
|
|
513
805
|
if (removeEventOps.length > 0)
|
|
514
806
|
cachedPage = undefined;
|
|
807
|
+
const resized = resizeOps.length > 0
|
|
808
|
+
? await applyResizes(platform, project, environment, applicationId, pageId, await readPage(), resizeOps)
|
|
809
|
+
: { applied: [], warnings: [] };
|
|
810
|
+
if (resizeOps.length > 0)
|
|
811
|
+
cachedPage = undefined;
|
|
812
|
+
const propTypes = propTypeOps.length > 0
|
|
813
|
+
? await applyPropTypes(platform, project, environment, applicationId, pageId, await readPage(), propTypeOps)
|
|
814
|
+
: [];
|
|
815
|
+
if (propTypeOps.length > 0)
|
|
816
|
+
cachedPage = undefined;
|
|
515
817
|
const response = opRecords.length > 0
|
|
516
818
|
? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: opRecords } })) ?? {})
|
|
517
819
|
: { valid: true };
|
|
820
|
+
if (view !== 'full')
|
|
821
|
+
outlineWriteResponse(response);
|
|
518
822
|
if (removedEvents.length > 0)
|
|
519
823
|
response.removedEvents = removedEvents;
|
|
824
|
+
if (propTypes.length > 0)
|
|
825
|
+
response.propTypes = propTypes;
|
|
826
|
+
if (resized.applied.length > 0)
|
|
827
|
+
response.resized = resized.applied;
|
|
828
|
+
if (resized.warnings.length > 0)
|
|
829
|
+
response.resizeWarnings = resized.warnings;
|
|
830
|
+
if (deletedFlows.length > 0)
|
|
831
|
+
response.deletedFlows = deletedFlows;
|
|
520
832
|
if (formWarnings.length > 0)
|
|
521
833
|
response.formWarnings = formWarnings;
|
|
522
834
|
if (flowWarnings.length > 0)
|
|
523
835
|
response.flowWarnings = flowWarnings;
|
|
524
836
|
if (layoutWarnings.length > 0)
|
|
525
837
|
response.layoutWarnings = layoutWarnings;
|
|
526
|
-
|
|
838
|
+
// G-23 micro-op ayağı: yama 200 döner ve `appliedPatch` op'un ŞEMAYA yazıldığını söyler, ama
|
|
839
|
+
// alanın gerçekten KAYDEDİLDİĞİNİ söylemez. Yamadan sonra şemayı bir kez okuyup op'un yazmak
|
|
840
|
+
// istediğiyle kayıtlı olanı karşılaştırıyoruz. Okuma başarısız olursa yama yine geçerlidir.
|
|
841
|
+
const contentOps = opRecords.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
|
|
842
|
+
let opDrops = [];
|
|
843
|
+
if (contentOps.length > 0) {
|
|
844
|
+
try {
|
|
845
|
+
const saved = await fetchPage(platform, project, environment, applicationId, pageId);
|
|
846
|
+
opDrops = schemaOpDrops(saved.schema, contentOps);
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
opDrops = [];
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return withWriteDiff(withNormalization(response, [
|
|
527
853
|
...rootAddsNote(retargeted, PAGE_CONTAINER_ELEMENT_ID),
|
|
528
854
|
...componentDefaultsNote(normalized.components),
|
|
529
855
|
...validationNote(normalized.validations),
|
|
530
|
-
...
|
|
531
|
-
...heightTypeNote(catalog.heights),
|
|
856
|
+
...catalogNotes,
|
|
532
857
|
...replacePropNote(normalized.props)
|
|
533
|
-
]);
|
|
858
|
+
]), opDrops);
|
|
534
859
|
}));
|
|
535
860
|
server.registerTool('page_create', {
|
|
536
861
|
title: 'Sayfa oluştur',
|
|
537
|
-
description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (
|
|
862
|
+
description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (canlı canvas ve hedefli düzeltme orada). Şemayı burada gönderirseniz 2026-09-08'den beri AYNI doğrulama duvarından geçer: tanınmayan prop adı ve geçersiz enum değeri 422 ile reddedilir ve sayfa HİÇ açılmaz. roleIds için role_list, layoutId için layout_list kullanın. Yanıttaki schema VARSAYILAN OLARAK iskelettir (çözülmüş layout düğümleri dâhil id/type/children'a iner) — tam gövde için view:'full'; gönderdiğiniz bir alan kaydedilmediyse silentlyDropped söyler.",
|
|
538
863
|
inputSchema: {
|
|
539
864
|
project: projectParam,
|
|
540
865
|
environment: environmentParam,
|
|
@@ -542,30 +867,24 @@ export function registerPageTools(server, platform) {
|
|
|
542
867
|
name: z.string().min(1),
|
|
543
868
|
slug: z.string().min(1).describe("URL yolu, örn. 'orders' ya da 'orders/detail'"),
|
|
544
869
|
browserTitle: z.string().optional(),
|
|
870
|
+
browserTitleKey: z.string().optional().describe("Sekme başlığının çeviri anahtarı. Verilirse başlık çeviri sözlüğünden çözülür ve language-selector ile dil değişince sayfa yenilenmeden güncellenir; boşsa browserTitle olduğu gibi kullanılır. Anahtarlar için translation_list."),
|
|
545
871
|
layoutId: guid.optional(),
|
|
546
872
|
roleIds: z.array(guid).optional().describe('Sayfayı görebilecek roller; boş = herkese açık'),
|
|
547
873
|
middlewareIds: z.array(guid).optional(),
|
|
548
874
|
events: eventsParam.optional(),
|
|
549
|
-
schema: schemaParam.optional()
|
|
875
|
+
schema: schemaParam.optional(),
|
|
876
|
+
view: writeViewParam
|
|
550
877
|
},
|
|
551
878
|
annotations: writes
|
|
552
|
-
}, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, layoutId, roleIds, middlewareIds, events, schema }) => {
|
|
553
|
-
const badEvents = invalidScreenEventNames(events, 'PAGE-011');
|
|
554
|
-
if (badEvents.length > 0)
|
|
555
|
-
throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
|
|
879
|
+
}, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, events, schema, view }) => {
|
|
556
880
|
const container = ensurePageContainer(schema ?? []);
|
|
557
|
-
|
|
558
|
-
if (badWidgets.length > 0)
|
|
559
|
-
throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
|
|
560
|
-
const nested = nestedRepeaterNodes(container.schema);
|
|
561
|
-
if (nested.length > 0)
|
|
562
|
-
throw new PlatformApiError(nestedRepeaterMessage(nested), 400);
|
|
881
|
+
assertScreenWriteGuards('page', { schema: container.schema, events });
|
|
563
882
|
const stringStyles = stringStyleNodes(container.schema);
|
|
564
883
|
const fixed = normalizeComponentDefaults(container.schema);
|
|
565
884
|
const reset = resetPageContainerPosition(container.schema);
|
|
566
|
-
const
|
|
567
|
-
? await
|
|
568
|
-
:
|
|
885
|
+
const catalogNotes = (schema ?? []).length > 0
|
|
886
|
+
? await screenCatalogNotes(platform, project, environment, applicationId, container.schema)
|
|
887
|
+
: [];
|
|
569
888
|
const moved = reorderSchemaBlocks(container.schema);
|
|
570
889
|
const gaps = (schema ?? []).length > 0 ? verticalGapWarnings(container.schema, 'PAGE-010 (uyarı)') : [];
|
|
571
890
|
const response = await platform.tenant('POST', project, `/pages/${applicationId}`, {
|
|
@@ -574,6 +893,7 @@ export function registerPageTools(server, platform) {
|
|
|
574
893
|
name,
|
|
575
894
|
slug,
|
|
576
895
|
browserTitle: browserTitle ?? '',
|
|
896
|
+
browserTitleKey: browserTitleKey ?? '',
|
|
577
897
|
layoutId: layoutId ?? null,
|
|
578
898
|
roleIds: roleIds ?? [],
|
|
579
899
|
middlewareIds: middlewareIds ?? [],
|
|
@@ -581,8 +901,20 @@ export function registerPageTools(server, platform) {
|
|
|
581
901
|
schema: container.schema
|
|
582
902
|
}
|
|
583
903
|
});
|
|
584
|
-
const
|
|
585
|
-
|
|
904
|
+
const saved = (response && typeof response === 'object' && !Array.isArray(response)
|
|
905
|
+
? (response.data ?? response)
|
|
906
|
+
: {});
|
|
907
|
+
// Kırpmadan ÖNCE doğrulama yolu (B-55): yanıt gövdesi iskelete inince "gönderdiğim prop
|
|
908
|
+
// kaydedildi mi" sorusunu gövde değil bu diff cevaplar. `layout_create`'in yaptığının sayfa
|
|
909
|
+
// ikizi; Prop sarmalı açılmazsa doğru kaydedilmiş her prop düşmüş görünür (G-28).
|
|
910
|
+
const drops = (schema ?? []).length > 0
|
|
911
|
+
? silentlyDropped({ schema: unwrapProps(container.schema) }, { schema: unwrapProps(saved.schema) })
|
|
912
|
+
: [];
|
|
913
|
+
if (view !== 'full' && Array.isArray(saved.schema)) {
|
|
914
|
+
saved.schema = saved.schema.map(node => summarize(node));
|
|
915
|
+
}
|
|
916
|
+
const normalized = withWriteDiff(withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), drops);
|
|
917
|
+
const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema)), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
|
|
586
918
|
if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
|
|
587
919
|
return normalized;
|
|
588
920
|
return { ...normalized, layoutWarnings: createWarnings };
|
|
@@ -598,6 +930,7 @@ export function registerPageTools(server, platform) {
|
|
|
598
930
|
name: z.string().min(1).optional(),
|
|
599
931
|
slug: z.string().min(1).optional(),
|
|
600
932
|
browserTitle: z.string().optional(),
|
|
933
|
+
browserTitleKey: z.string().optional().describe("Sekme başlığının çeviri anahtarı. Verilirse başlık çeviri sözlüğünden çözülür ve language-selector ile dil değişince sayfa yenilenmeden güncellenir; boşsa browserTitle olduğu gibi kullanılır. Anahtarlar için translation_list."),
|
|
601
934
|
layoutId: guid.nullable().optional().describe('null: layout bağını kaldırır'),
|
|
602
935
|
roleIds: z.array(guid).optional(),
|
|
603
936
|
middlewareIds: z.array(guid).optional(),
|
|
@@ -607,31 +940,15 @@ export function registerPageTools(server, platform) {
|
|
|
607
940
|
annotations: updates
|
|
608
941
|
}, handle(config, async ({ project, environment, applicationId, pageId, ...changes }) => {
|
|
609
942
|
const overrides = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== undefined));
|
|
610
|
-
|
|
611
|
-
if (badEvents.length > 0)
|
|
612
|
-
throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
|
|
613
|
-
const badWidgets = invalidWidgetTypeNodes(overrides.schema, 'PAGE-012');
|
|
614
|
-
if (badWidgets.length > 0)
|
|
615
|
-
throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
|
|
616
|
-
const nestedUpdate = nestedRepeaterNodes(overrides.schema);
|
|
617
|
-
if (nestedUpdate.length > 0)
|
|
618
|
-
throw new PlatformApiError(nestedRepeaterMessage(nestedUpdate), 400);
|
|
943
|
+
assertScreenWriteGuards('page', { schema: overrides.schema, events: overrides.events });
|
|
619
944
|
const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
|
|
620
|
-
const badSchemaNames = invalidComponentNames(overrides.schema);
|
|
621
|
-
if (badSchemaNames.length > 0) {
|
|
622
|
-
throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badSchemaNames.join(' · ')}. `
|
|
623
|
-
+ 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; geçersiz bir ad sayfadaki TÜM ifadeleri öldürür. '
|
|
624
|
-
+ 'name = elementId yazın.', 400);
|
|
625
|
-
}
|
|
626
945
|
const fixed = normalizeComponentDefaults(overrides.schema);
|
|
627
946
|
const reset = overrides.schema === undefined ? [] : resetPageContainerPosition(overrides.schema);
|
|
628
|
-
const
|
|
629
|
-
? { clamped: [], heights: [] }
|
|
630
|
-
: await applyCatalogLayoutRules(platform, project, environment, applicationId, overrides.schema);
|
|
947
|
+
const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, overrides.schema);
|
|
631
948
|
const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
|
|
632
949
|
const gaps = overrides.schema === undefined
|
|
633
950
|
? []
|
|
634
|
-
: [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema))];
|
|
951
|
+
: [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema)), ...flexSizeWarnings(overrides.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)')];
|
|
635
952
|
const body = pageUpdateBody(current, overrides);
|
|
636
953
|
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
637
954
|
const summary = {
|
|
@@ -641,7 +958,7 @@ export function registerPageTools(server, platform) {
|
|
|
641
958
|
};
|
|
642
959
|
if (gaps.length > 0)
|
|
643
960
|
summary.layoutWarnings = gaps;
|
|
644
|
-
return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...
|
|
961
|
+
return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]);
|
|
645
962
|
}));
|
|
646
963
|
server.registerTool('page_clone', {
|
|
647
964
|
title: 'Sayfa klonla',
|