@appaflytech/zeyrek-ai-mcp 0.0.16 → 0.0.18
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/README.md +14 -13
- package/dist/knowledge/rules.js +21 -0
- package/dist/platform/diagnostics.js +85 -0
- package/dist/platform/ifExists.js +129 -0
- package/dist/platform/responses.js +2 -1
- package/dist/platform/strictArguments.js +22 -2
- package/dist/platform/writeDiff.js +58 -31
- package/dist/platform/writeView.js +17 -0
- package/dist/resources/guides/functions.md +31 -2
- package/dist/resources/guides/getting-started.md +35 -0
- package/dist/resources/schema-fingerprint.json +3 -3
- package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +18 -0
- package/dist/tools/designSchemaPatch.js +3 -3
- package/dist/tools/flows.js +35 -41
- package/dist/tools/functions.js +20 -10
- package/dist/tools/microservices.js +39 -11
- package/dist/tools/pages.js +55 -12
- package/dist/tools/workflows.js +39 -12
- package/package.json +6 -1
|
@@ -153,6 +153,21 @@ koda baktığını gizlemek yerine raporlamak.
|
|
|
153
153
|
(GW-005). Bir argüman kabul edildiyse gateway onu okuyor demektir.
|
|
154
154
|
|
|
155
155
|
|
|
156
|
+
## Aynı çağrıyı iki kez yapmak kopya açmaz: `ifExists`
|
|
157
|
+
|
|
158
|
+
Platform aynı adla ikinci kaydı hata vermeden `"<ad> (1)"` olarak açar. `flow_create`,
|
|
159
|
+
`microservice_create`, `workflow_create` ve `page_create` bu yüzden yazmadan önce aynı ad ya da
|
|
160
|
+
slug'ı (harf duyarsız) arar:
|
|
161
|
+
|
|
162
|
+
| `ifExists` | Kayıt zaten varsa |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `error` (varsayılan) | Yazmaz; hata mevcut id'yi ve varsa `(n)` sayaçlı kopyaları söyler |
|
|
165
|
+
| `update` | Mevcut kaydı günceller, yanıtta `outcome: 'updated'`. `page_create`'te yalnız verilen alanlar yazılır, şema korunur. `workflow_create`'te mevcut kaydın `type`'ı farklıysa (middleware ↔ automation) yazmaz — tür değişirdi |
|
|
166
|
+
| `duplicate` | Platform davranışı: ikinci kayıt açılır. Platformun eklediği sayaç (ad `' (1)'`, slug `-1`; akışta ikisi de `' (1)'`) kayıp değildir, yanıtta `renamedByPlatform`'dadır |
|
|
167
|
+
|
|
168
|
+
Betiği yeniden koşturacaksan `ifExists: 'update'` ver; kopyaları elle ayıklayan yardımcı kod
|
|
169
|
+
yazma. Mevcut liste okunamazsa çağrı yazmadan düşer — tekrar dene.
|
|
170
|
+
|
|
156
171
|
## Yazdıktan sonra `silentlyDropped` alanına bak
|
|
157
172
|
|
|
158
173
|
Platformun bir alanı **hatasız yutması** bu depodaki en pahalı hata sınıfı: çağrı 200 döner,
|
|
@@ -169,6 +184,26 @@ Gateway yazma tool'larında gönderdiğin gövdeyi kaydedilenle karşılaştır
|
|
|
169
184
|
]
|
|
170
185
|
```
|
|
171
186
|
|
|
187
|
+
Yazma yanıtında bu liste ve `gatewayNormalized` **kısaltılmış** gelir: aynı yolun düşmeleri tek
|
|
188
|
+
satırda sayıyla toplanır (`selectFields[*].path · kaydedilmedi: 25 öğe — ör. …`). Kayıp sayısı
|
|
189
|
+
`droppedCount`'tadır; sayım 1000 farkta durursa `droppedCountCapped: true` gelir ve sayı "en az"
|
|
190
|
+
demektir. Liste dışı bilgiler (20 satır kesmesi, sıra tabanlı karşılaştırma uyarısı)
|
|
191
|
+
`silentlyDroppedNotes`'tadır ve sayıya girmez. Tam liste gerekiyorsa yazmayı tekrarlama — kaydı `*_get` ile oku,
|
|
192
|
+
sonraki yazmalarda `diagnostics: 'full'` ver. `microservice_*` ve `workflow_*` create/update
|
|
193
|
+
yanıtında `data` yalnız kimlik alanlarını taşır; gövdenin tamamı için `view: 'full'`.
|
|
194
|
+
|
|
195
|
+
**Betikten toplu yazarken yanıtı olduğu gibi basma.** Otuz yazmanın tam yanıtı çıktı sınırını aşar
|
|
196
|
+
ve dosyaya düşer. Her çağrı için tek satır bas; ayrıntıyı yalnız sorunlu çağrıda bas:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const r = JSON.parse(result.content[0].text)
|
|
200
|
+
if (result.isError || r.droppedCount > 0) console.log(JSON.stringify({ slug, error: result.isError ? r : undefined, droppedCount: r.droppedCount, silentlyDropped: r.silentlyDropped }))
|
|
201
|
+
else console.log(JSON.stringify({ slug, id: r.data?.id, outcome: r.outcome }))
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Yalnız `isError`'a bakma: platform alanı hatasız yutunca çağrı başarılıdır ve kayıp yalnız
|
|
205
|
+
`droppedCount`'ta görünür.
|
|
206
|
+
|
|
172
207
|
Diff dizi öğelerini indeksle değil KİMLİKLE eşler (`stepId`, `elementId`, `name`, `id`), çünkü
|
|
173
208
|
platform diziyi kendi sırasında geri döndürür. Diff üretilemeyen tek durum, yanıtın kayıt değil
|
|
174
209
|
kabul gövdesi olmasıdır; o zaman alan hiç görünmez ve doğrulamayı `*_get` ile kendin yaparsın.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
3
|
-
"generatedAt": "2026-09-
|
|
2
|
+
"hash": "f37265d598ea468deb2157165891629c2e88c7fd2238e82ec60cda7f58f7de4c",
|
|
3
|
+
"generatedAt": "2026-09-18T12:57:15.416Z",
|
|
4
4
|
"types": 186,
|
|
5
5
|
"files": 108,
|
|
6
6
|
"note": "Şemaları üreten backend kaynaklarının parmak izi. Elle düzenlemeyin: 'npm run generate-schemas' yazar, 'npm run schema:check' denetler.",
|
|
7
7
|
"missing": [
|
|
8
|
-
"NgApps.Infrastructure.Models.IdNamePair`1[[System.Guid, System.Private.CoreLib, Version=
|
|
8
|
+
"NgApps.Infrastructure.Models.IdNamePair`1[[System.Guid, System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]"
|
|
9
9
|
]
|
|
10
10
|
}
|
|
@@ -95,6 +95,20 @@
|
|
|
95
95
|
"$ref": "#/components/schemas/NgApps.Domain.Contexts.Shared.Dependencies.DependencyNode"
|
|
96
96
|
},
|
|
97
97
|
"nullable": true
|
|
98
|
+
},
|
|
99
|
+
"bindings": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"items": {
|
|
102
|
+
"$ref": "#/components/schemas/NgApps.Domain.Contexts.Shared.Dependencies.DependencyNode"
|
|
103
|
+
},
|
|
104
|
+
"nullable": true
|
|
105
|
+
},
|
|
106
|
+
"functions": {
|
|
107
|
+
"type": "array",
|
|
108
|
+
"items": {
|
|
109
|
+
"$ref": "#/components/schemas/NgApps.Domain.Contexts.Shared.Dependencies.DependencyNode"
|
|
110
|
+
},
|
|
111
|
+
"nullable": true
|
|
98
112
|
}
|
|
99
113
|
},
|
|
100
114
|
"additionalProperties": false
|
|
@@ -189,6 +203,10 @@
|
|
|
189
203
|
},
|
|
190
204
|
"stepKind": {
|
|
191
205
|
"$ref": "#/components/schemas/NgApps.Domain.Contexts.Studio.Flows.StepKind"
|
|
206
|
+
},
|
|
207
|
+
"propertyName": {
|
|
208
|
+
"type": "string",
|
|
209
|
+
"nullable": true
|
|
192
210
|
}
|
|
193
211
|
},
|
|
194
212
|
"additionalProperties": false
|
|
@@ -3,7 +3,7 @@ import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collec
|
|
|
3
3
|
import { guid, projectParam, environmentParam, updates, microOps } from './common.js';
|
|
4
4
|
import { fetchHost, hostPath } from './screens.js';
|
|
5
5
|
import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
|
|
6
|
-
import { schemaOpDrops, withWriteDiff } from '../platform/writeDiff.js';
|
|
6
|
+
import { emptyWriteDiff, schemaOpDrops, withWriteDiff } from '../platform/writeDiff.js';
|
|
7
7
|
import { fetchComponentEventNames } from './components.js';
|
|
8
8
|
import { PlatformApiError } from '../platform/errors.js';
|
|
9
9
|
import { handle } from '../platform/responses.js';
|
|
@@ -136,14 +136,14 @@ export function registerDesignSchemaPatchTool(server, platform, host) {
|
|
|
136
136
|
if (layoutWarnings.length > 0)
|
|
137
137
|
response.layoutWarnings = layoutWarnings;
|
|
138
138
|
const contentOps = opRecords.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
|
|
139
|
-
let opDrops =
|
|
139
|
+
let opDrops = emptyWriteDiff();
|
|
140
140
|
if (contentOps.length > 0) {
|
|
141
141
|
try {
|
|
142
142
|
const saved = await fetchHost(platform, host, project, environment, applicationId, id);
|
|
143
143
|
opDrops = schemaOpDrops(saved[spec.tree], contentOps);
|
|
144
144
|
}
|
|
145
145
|
catch {
|
|
146
|
-
opDrops =
|
|
146
|
+
opDrops = emptyWriteDiff();
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
return withWriteDiff(withNormalization(response, [
|
package/dist/tools/flows.js
CHANGED
|
@@ -5,12 +5,13 @@ import { flowBindings, normalizeFlowSteps, targetlessSetterWarnings, unknownSett
|
|
|
5
5
|
import { flowBodyWarnings } from '../knowledge/coverage.js';
|
|
6
6
|
import { PlatformApiError } from '../platform/errors.js';
|
|
7
7
|
import { handle } from '../platform/responses.js';
|
|
8
|
-
import { writeDiff, withWriteDiff } from '../platform/writeDiff.js';
|
|
8
|
+
import { emptyWriteDiff, writeDiff, withWriteDiff } from '../platform/writeDiff.js';
|
|
9
9
|
import { resolvePatchOperations } from '../knowledge/outlines.js';
|
|
10
10
|
import { patchResult, baseVersionOf } from '../platform/patch.js';
|
|
11
11
|
import fastJsonPatch from 'fast-json-patch';
|
|
12
12
|
import { policyRules, flowExpressionSites } from '../knowledge/rules.js';
|
|
13
13
|
import { functionCallRefs } from '../knowledge/functionGuards.js';
|
|
14
|
+
import { existingMatch, ifExistsParam, platformRename, withRename } from '../platform/ifExists.js';
|
|
14
15
|
import { randomUUID } from 'node:crypto';
|
|
15
16
|
import { z } from 'zod';
|
|
16
17
|
/**
|
|
@@ -19,14 +20,18 @@ import { z } from 'zod';
|
|
|
19
20
|
* `flow_create` 200 döndü, `flow_get` adımı gösterdi ama `args` içinde kodu taşıyan alan yoktu —
|
|
20
21
|
* teşhis ~10 dakika sürdü ve tek belirti filtrenin hiçbir şey yapmamasıydı. Okuma başarısız
|
|
21
22
|
* olursa yazma yine başarılıdır: diff bir teşhis eklentisidir, yazmanın sonucu değil.
|
|
23
|
+
*
|
|
24
|
+
* Okunan kayıt da döner: `ifExists:'duplicate'` yazmasında platform ada/slug'a ' (1)' ekler ve yanıtın
|
|
25
|
+
* gönderilen adı değil KAYDEDİLEN adı göstermesi gerekir (flow-upsert-ve-baglar S-10).
|
|
22
26
|
*/
|
|
23
|
-
async function flowWriteDrops(platform, host, project, environment, applicationId, hostId, flowId, sent) {
|
|
27
|
+
async function flowWriteDrops(platform, host, project, environment, applicationId, hostId, flowId, sent, allowRename = false) {
|
|
24
28
|
try {
|
|
25
29
|
const saved = unwrap(await platform.tenant('GET', project, flowPath(host, applicationId, hostId, flowId), { environment }));
|
|
26
|
-
|
|
30
|
+
const rename = allowRename ? platformRename(sent, saved, 'paren') : { sent };
|
|
31
|
+
return { ...writeDiff(rename.sent, saved), saved, ...(rename.renamed ? { renamed: rename.renamed } : {}) };
|
|
27
32
|
}
|
|
28
33
|
catch {
|
|
29
|
-
return
|
|
34
|
+
return emptyWriteDiff();
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
const stepsParam = z.array(z.record(z.unknown())).describe("Adım listesi (StepManagementSchema: { stepId, name, type, kind, args, branches }). DİKKAT: branches[].nextStepId bir GUID değil, hedef adımın stepId STRING'idir; dal ADI motorun anahtarıdır ve start/setter/code/function adımlarında 'always' olmak zorundadır. Tam şablon: ngapps://examples/flow-load-table (ya da flow_get çıktısı); adım tipleri ve setter/parameters şekli platform_guide('page') Kural 1c. Kullanıcı fonksiyonu çağırmak için 'function' adımı: { type:'function', args:{ functionId:'<function_list id>', functionName:'<ad>', parameters:[{ name:'<bildirimdeki ad>', value:{ type:'static'|'property'|'storage'|'state'|'code', ref } }] } } → çıktı Steps.<adımAdı>.data (ngapps://examples/flow-call-function, platform_guide('functions')).");
|
|
@@ -69,8 +74,7 @@ function flowListResult(flows, record) {
|
|
|
69
74
|
...(danglingBindings.length > 0 ? { danglingBindings } : {})
|
|
70
75
|
};
|
|
71
76
|
}
|
|
72
|
-
const
|
|
73
|
-
const upper = (value) => String(value ?? '').toUpperCase();
|
|
77
|
+
const flowIfExistsParam = ifExistsParam('akış', 'mevcut akışı bu gövdeyle günceller (id\'si ve olay bağları korunur)');
|
|
74
78
|
/**
|
|
75
79
|
* Barındırıcının akışları, önbelleksiz liste ucundan (`GET /pages|layouts/{id}/flows`). Layout ve
|
|
76
80
|
* sayfa tasarım kaydı önbellekten gelir ve zarfsız akış yazmasından sonra akışı hemen taşımaz;
|
|
@@ -103,36 +107,21 @@ async function listHostFlows(platform, host, project, environment, applicationId
|
|
|
103
107
|
* tam da zaman aşımında kopya üretirdi.
|
|
104
108
|
*/
|
|
105
109
|
async function existingFlowMatch(platform, host, project, environment, applicationId, hostId, hostRecord, name, slug, flowId, ifExists) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const copies = flows.filter(flow => copyPattern.test(String(flow.name ?? '')));
|
|
122
|
-
const copyNote = copies.length > 0
|
|
123
|
-
? ` Barındırıcıda bu adın sayaçlı eşleri de var: ${copies.map(describe).join(' · ')} — bağı olup olmadığını flow_list (bindings) ile kontrol edip gerekirse silin.`
|
|
124
|
-
: '';
|
|
125
|
-
if (ifExists === 'error') {
|
|
126
|
-
const where = host === 'page' ? 'sayfada' : host === 'layout' ? "layout'ta" : "widget'ta";
|
|
127
|
-
throw new PlatformApiError(`FLOW-026: bu ${where} aynı ad ya da slug'da akış zaten var: ${matches.map(describe).join(' · ')}. İstek gönderilmedi — gönderilseydi platform ürün kuralı gereği ikinci bir akış açardı (çakışan ad ya da slug'a ' (1)' sayacı eklenerek; yalnız harf farkı varsa sayaçsız). Mevcut akışı güncellemek için aynı çağrıyı ifExists:'update' ile tekrarlayın; bilerek ikinci akış istiyorsanız ifExists:'duplicate'.${copyNote}`, 409);
|
|
128
|
-
}
|
|
129
|
-
if (matches.length > 1) {
|
|
130
|
-
throw new PlatformApiError(`FLOW-026: ad ve slug farklı akışlara çıkıyor (${matches.map(describe).join(' · ')}); hangisinin güncelleneceği belirsiz, istek gönderilmedi. flowId ile flow_update kullanın.${copyNote}`, 409);
|
|
131
|
-
}
|
|
132
|
-
if (flowId !== undefined && flowId.toLowerCase() !== String(match.id).toLowerCase()) {
|
|
133
|
-
throw new PlatformApiError(`FLOW-026: verilen flowId (${flowId}) aynı adlı mevcut akışla (${describe(match)}) çelişiyor; istek gönderilmedi. Mevcut akışı güncellemek için flowId'yi vermeyin ya da ${String(match.id)} verin.`, 409);
|
|
134
|
-
}
|
|
135
|
-
return match;
|
|
110
|
+
return await existingMatch({
|
|
111
|
+
code: 'FLOW-026',
|
|
112
|
+
noun: 'akış',
|
|
113
|
+
where: host === 'page' ? 'bu sayfada' : host === 'layout' ? "bu layout'ta" : "bu widget'ta",
|
|
114
|
+
name,
|
|
115
|
+
slug,
|
|
116
|
+
id: flowId,
|
|
117
|
+
idParam: 'flowId',
|
|
118
|
+
ifExists,
|
|
119
|
+
list: () => listHostFlows(platform, host, project, environment, applicationId, hostId, async () => hostRecord),
|
|
120
|
+
listTool: 'flow_list',
|
|
121
|
+
copyCheck: 'bağı olup olmadığını flow_list (bindings) ile kontrol edip gerekirse silin',
|
|
122
|
+
updateTool: 'flowId ile flow_update',
|
|
123
|
+
slugSuffix: 'paren'
|
|
124
|
+
});
|
|
136
125
|
}
|
|
137
126
|
async function requiredParameterWarnings(platform, project, environment, applicationId, steps) {
|
|
138
127
|
const warnings = [];
|
|
@@ -356,7 +345,7 @@ export function registerFlowTools(server, platform) {
|
|
|
356
345
|
steps: stepsParam,
|
|
357
346
|
errorSteps: stepsParam.optional(),
|
|
358
347
|
flowId: guid.optional().describe('Verilmezse platform üretir; şemaya önceden bağlamak için kendiniz üretebilirsiniz'),
|
|
359
|
-
ifExists:
|
|
348
|
+
ifExists: flowIfExistsParam,
|
|
360
349
|
policyOverride: policyOverrideParam
|
|
361
350
|
},
|
|
362
351
|
annotations: writes
|
|
@@ -392,15 +381,20 @@ export function registerFlowTools(server, platform) {
|
|
|
392
381
|
}
|
|
393
382
|
// Diff YALNIZ çağıranın yazdığı alanları kapsar; `id` ve barındırıcı bağı (`pageId` vb.)
|
|
394
383
|
// gövdeye gateway koyar, kullanıcının düzeltebileceği bir şey değildir.
|
|
395
|
-
const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, writtenFlowId, { name: flow.name, slug: flow.slug, steps: flow.steps, errorSteps: flow.errorSteps });
|
|
396
|
-
return attachFindings(withWriteDiff(withNormalization({
|
|
384
|
+
const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, writtenFlowId, { name: flow.name, slug: flow.slug, steps: flow.steps, errorSteps: flow.errorSteps }, ifExists === 'duplicate');
|
|
385
|
+
return attachFindings(withWriteDiff(withRename(withNormalization({
|
|
397
386
|
host,
|
|
398
387
|
hostId,
|
|
399
388
|
outcome: existing === undefined ? 'created' : 'updated',
|
|
400
|
-
flow: {
|
|
389
|
+
flow: {
|
|
390
|
+
id: writtenFlowId,
|
|
391
|
+
name: typeof drops.saved?.name === 'string' ? drops.saved.name : name,
|
|
392
|
+
slug: typeof drops.saved?.slug === 'string' ? drops.saved.slug : slug,
|
|
393
|
+
steps: steps.length
|
|
394
|
+
},
|
|
401
395
|
...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
|
|
402
396
|
hint: "Akışı bir olaya bağlamak için: page_schema_patch (layout'ta layout_schema_patch, widget'ta component_schema_patch) ops:[{op:'set_event', target:<elementId>, event:'<component_get events listesindeki ad>', eventValue:<flow id>}]; olayda zaten akış bağlıysa ikinciyi add_event ile ekleyin (set_event bağlı akışı değiştirir, PAGE-034). Olay adları komponente özeldir ve 'on' ÖNEKİ ALMAZ (button → 'click', form → 'submit', table → 'mount'/'select'/'sort'…); uydurma ad 200 döner ama bağ sessizce ölü kalır — gateway PAGE-008 ile yakalar."
|
|
403
|
-
}, normalized), drops), findings);
|
|
397
|
+
}, normalized), drops.renamed), drops), findings);
|
|
404
398
|
}));
|
|
405
399
|
server.registerTool('flow_update', {
|
|
406
400
|
title: 'Akış güncelle',
|
package/dist/tools/functions.js
CHANGED
|
@@ -16,7 +16,7 @@ import { z } from 'zod';
|
|
|
16
16
|
const sourceParam = z.enum(['workflow', 'studio']).describe("Nerede çalışır: 'workflow' → SUNUCU (workflow/middleware/automation ifadeleri, Jint), tenant geneli, çıplak adla çağrılır (toplamHesapla(1, 2)), applicationId/scope/pageId VERİLMEZ, isAsync olamaz. 'studio' → TARAYICI ({{ }} bağlaması, UI flow code/function adımı), uygulamaya ait, Functions.toplamHesapla(1, 2) ile çağrılır, applicationId + scope zorunlu. Kaynak sonradan DEĞİŞMEZ");
|
|
17
17
|
const scopeParam = z.enum(['application', 'page']).describe("Yalnız source:'studio'. application: uygulamanın her sayfasında görünür. page: yalnız o sayfanın tarayıcı bağlamında, pageId zorunlu (K2). Ad uygulama içinde kapsamlar arası benzersizdir (K3)");
|
|
18
18
|
const nameParam = z.string().min(1).describe("Fonksiyon adı; studio'da Functions.<ad>(…), workflow'da <ad>(…) ile çağrılır. JS tanımlayıcısı olmalı, ayrılmış sözcük olamaz; studio'da 'Functions', workflow'da motor global adları (system, input, Now…) yasaktır (FN-001, istek gitmeden). Aynı ad iki kaynakta yan yana yaşayabilir (ayrı ad alanları); aynı kaynak içinde tekrarı platform 422 ile reddeder");
|
|
19
|
-
const bodyParam = z.string().min(1).max(65536).describe("Fonksiyon GÖVDESİ, iki kaynakta aynı biçim: düz JavaScript deyimleri, 'return' içerebilir — 'function' sarmalı ya da arrow YAZILMAZ ('return fiyat * adet'). Gövde çağrıldığı yerin bağlamını kapanış olarak görür: tarayıcıda Page/state/Steps/komponent adları, sunucuda input/adım çıktıları/system.* (K7). Parametre adları gövdede doğrudan kullanılır. Platform kaydetmeden önce SÖZDİZİMİNİ denetler (422 GlobalFunctionBodyInvalid, ayrıştırıcı mesajıyla). En çok 64 KB");
|
|
19
|
+
const bodyParam = z.string().min(1).max(65536).describe("Fonksiyon GÖVDESİ, iki kaynakta aynı biçim: düz JavaScript deyimleri, 'return' içerebilir — 'function' sarmalı ya da arrow YAZILMAZ ('return fiyat * adet'). Gövde çağrıldığı yerin bağlamını kapanış olarak görür: tarayıcıda Page/state/Steps/komponent adları, sunucuda input/adım çıktıları/system.* (K7). Parametre adları gövdede doğrudan kullanılır. Gövde YORUMLU yazılır: başta ne yaptığını/parametreleri/dönüşü söyleyen // baş yorumu, açık olmayan adımlarda kısa satır yorumu; 5+ dolu satırlı yorumsuz gövde FN-006 uyarısı alır (Studio yorumu yeşil çizer). Platform kaydetmeden önce SÖZDİZİMİNİ denetler (422 GlobalFunctionBodyInvalid, ayrıştırıcı mesajıyla). En çok 64 KB");
|
|
20
20
|
const parameterParam = z.object({
|
|
21
21
|
name: z.string().min(1).describe('Parametre adı — JS tanımlayıcısı, fonksiyon içinde benzersiz; gövdede bu adla okunur'),
|
|
22
22
|
type: z.enum(['text', 'number', 'boolean', 'date', 'object', 'array', 'any']).optional()
|
|
@@ -64,7 +64,10 @@ export function describeSignatureChange(current, next) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
function signatureWarning(name, source, change, dependents) {
|
|
67
|
-
const callers = [
|
|
67
|
+
const callers = ['flows', 'workflows', 'bindings', 'functions']
|
|
68
|
+
.map(bucket => (dependents[bucket].length ? `${bucket}: ${dependents[bucket].join(', ')}` : ''))
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join('; ');
|
|
68
71
|
const what = [
|
|
69
72
|
change.name ? `ad '${change.name[0]}' → '${change.name[1]}'` : '',
|
|
70
73
|
change.parameters ? `parametreler [${change.parameters[0].join(', ')}] → [${change.parameters[1].join(', ')}]` : ''
|
|
@@ -75,11 +78,12 @@ function signatureWarning(name, source, change, dependents) {
|
|
|
75
78
|
return `İmza değişti (${what}) ve fonksiyonu ${dependents.total} kayıt kullanıyor (${callers}): ${effect}. Güncelleme yapıldı; çağıran ifadeleri gözden geçirin — dependency_graph(module:'global-function').`;
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
|
-
* `GET global-functions/{id}/dependencies` KOVALI grafiği döner (`{ flows, workflows }`); düz dizi
|
|
81
|
+
* `GET global-functions/{id}/dependencies` KOVALI grafiği döner (`{ flows, workflows, bindings, functions }`); düz dizi
|
|
79
82
|
* şekli de düzleştirilir — yapısal (`args.functionId`) ve metinsel (`Functions.<ad>(` / çıplak `<ad>(`)
|
|
80
83
|
* bağımlılıklar aynı listede gelir. Etiket Studio rozetleriyle aynıdır: komponent olayı `Sahip › Komponent · olay`,
|
|
81
84
|
* sayfa/layout olayı `Sahip · olay`, bağı olmayan akış `Sahip › akış adı` (sahip sayfa/layout/widget; eski yanıtta
|
|
82
|
-
* sahipsiz); automation düğümü `ad (automation)`.
|
|
85
|
+
* sahipsiz); automation düğümü `ad (automation)`. Kod tipli prop bağlaması `Sahip › Komponent · prop`, gövdesinde
|
|
86
|
+
* çağıran fonksiyon adıyla yazılır.
|
|
83
87
|
*/
|
|
84
88
|
export function summarizeDependents(payload) {
|
|
85
89
|
const data = (() => {
|
|
@@ -94,7 +98,7 @@ export function summarizeDependents(payload) {
|
|
|
94
98
|
const name = String(entry.name ?? entry.slug ?? entry.id ?? '?');
|
|
95
99
|
const host = text(entry.hostName);
|
|
96
100
|
const component = text(entry.componentName);
|
|
97
|
-
const event = text(entry.eventTitle) ?? text(entry.eventName);
|
|
101
|
+
const event = text(entry.eventTitle) ?? text(entry.eventName) ?? text(entry.propertyName);
|
|
98
102
|
const label = component
|
|
99
103
|
? [host, [component, event].filter(Boolean).join(' · ')].filter(Boolean).join(' › ')
|
|
100
104
|
: event
|
|
@@ -106,14 +110,20 @@ export function summarizeDependents(payload) {
|
|
|
106
110
|
const entries = data.filter(isRecord);
|
|
107
111
|
const moduleOf = (entry) => String(entry.module ?? entry.type ?? entry.kind ?? '').toLowerCase();
|
|
108
112
|
const isWorkflow = (entry) => moduleOf(entry).includes('workflow') || moduleOf(entry) === 'automation';
|
|
113
|
+
const isFunction = (entry) => moduleOf(entry) === 'global-function';
|
|
114
|
+
const isBinding = (entry) => text(entry.propertyName) !== undefined;
|
|
109
115
|
const workflows = names(entries.filter(isWorkflow));
|
|
110
|
-
const
|
|
111
|
-
|
|
116
|
+
const functions = names(entries.filter(isFunction));
|
|
117
|
+
const bindings = names(entries.filter(entry => !isWorkflow(entry) && !isFunction(entry) && isBinding(entry)));
|
|
118
|
+
const flows = names(entries.filter(entry => !isWorkflow(entry) && !isFunction(entry) && !isBinding(entry)));
|
|
119
|
+
return { flows, workflows, bindings, functions, total: flows.length + workflows.length + bindings.length + functions.length };
|
|
112
120
|
}
|
|
113
121
|
const record = isRecord(data) ? data : {};
|
|
114
122
|
const flows = names(record.flows);
|
|
115
123
|
const workflows = names(record.workflows);
|
|
116
|
-
|
|
124
|
+
const bindings = names(record.bindings);
|
|
125
|
+
const functions = names(record.functions);
|
|
126
|
+
return { flows, workflows, bindings, functions, total: flows.length + workflows.length + bindings.length + functions.length };
|
|
117
127
|
}
|
|
118
128
|
function usageHint(name, source, scope) {
|
|
119
129
|
if (source === 'workflow') {
|
|
@@ -268,7 +278,7 @@ export function registerFunctionTools(server, platform) {
|
|
|
268
278
|
data: saved,
|
|
269
279
|
signature: functionSignature(saved.name ? saved : definition),
|
|
270
280
|
signatureChange,
|
|
271
|
-
dependents: dependents ?? { flows: [], workflows: [], total: 0 },
|
|
281
|
+
dependents: dependents ?? { flows: [], workflows: [], bindings: [], functions: [], total: 0 },
|
|
272
282
|
...(dependents === undefined ? { dependentsNote: 'Bağımlılık ucu okunamadı; çağıran akış olup olmadığı bilinmiyor.' } : {}),
|
|
273
283
|
...(renamedReferences !== undefined ? { renamedReferences } : {}),
|
|
274
284
|
...(dependents !== undefined && dependents.total > 0 && callersBreak
|
|
@@ -321,7 +331,7 @@ export function registerFunctionTools(server, platform) {
|
|
|
321
331
|
return {
|
|
322
332
|
deleted: id,
|
|
323
333
|
source,
|
|
324
|
-
dependents: dependents ?? { flows: [], workflows: [], total: 0 },
|
|
334
|
+
dependents: dependents ?? { flows: [], workflows: [], bindings: [], functions: [], total: 0 },
|
|
325
335
|
...(dependents === undefined ? { dependentsNote: 'Bağımlılık ucu okunamadı; çağıran akış olup olmadığı bilinmiyor.' } : {}),
|
|
326
336
|
...(dependents !== undefined && dependents.total > 0
|
|
327
337
|
? { note: `${dependents.total} çağıran kırıldı (acknowledgeDependents): ${[...dependents.flows, ...dependents.workflows].join(', ')} — ifadeleri düzeltin ya da fonksiyonu aynı adla yeniden tanımlayın.` }
|
|
@@ -10,11 +10,15 @@ import { policyRules } from '../knowledge/rules.js';
|
|
|
10
10
|
import { writeDiff, withWriteDiff } from '../platform/writeDiff.js';
|
|
11
11
|
import { identityHeaders } from '../platform/tokenVault.js';
|
|
12
12
|
import { PlatformApiError } from '../platform/errors.js';
|
|
13
|
+
import { existingMatch, ifExistsParam, listAll, platformRename, withOutcome, withRename } from '../platform/ifExists.js';
|
|
14
|
+
import { outlineWrite, recordWriteViewParam } from '../platform/writeView.js';
|
|
13
15
|
import { handle } from '../platform/responses.js';
|
|
14
16
|
import fastJsonPatch from 'fast-json-patch';
|
|
15
17
|
import { z } from 'zod';
|
|
16
18
|
import { constraintSummary } from '../knowledge/backendConstraints.js';
|
|
17
19
|
import { unmappedMembers, introducedUnmappedMembers, withUnmappedMembers } from '../knowledge/schemaGuard.js';
|
|
20
|
+
const MICROSERVICE_OUTLINE_KEYS = ['url', 'httpMethod', 'source', 'type'];
|
|
21
|
+
const microserviceIfExistsParam = ifExistsParam('uç', 'mevcut ucu bu tanımla tam gövde günceller (id\'si korunur; microservice_update ile aynı yazma)');
|
|
18
22
|
const policyOverrideParam = z.boolean().optional()
|
|
19
23
|
.describe('true: politika block bulgularını warn\'a indirir (bilinçli istisna; yanıtta işaretlenir)');
|
|
20
24
|
/**
|
|
@@ -287,28 +291,52 @@ export function registerMicroserviceTools(server, platform) {
|
|
|
287
291
|
}));
|
|
288
292
|
server.registerTool('microservice_create', {
|
|
289
293
|
title: 'Microservice oluştur',
|
|
290
|
-
description: "Yeni microservice tanımlar (definition = CreateMicroserviceCommand). İlk kullanımdan önce platform_guide('microservice') okuyun: kaynak seçimi (table > query) ve selectFields disiplini. 'description' ZORUNLUDUR (MS-014, block): çağıranın bu uçtan ne elde ettiğini yazın. Auth/oturum gerektiren uçlarda definition.middlewareIds ile middleware bağlayın; kimlik kapsaması filtrede source:'expression' + value:'user.id' ile yapılır (platform_guide('middleware')). BU YALNIZ source:'table' UÇLARDA VAR: query kaynaklı ucun parametreleri ÇAĞRIDAN gelir (body → query-string → header) ve middleware'in kurduğu user o yola girmez — kapsamı gövdeye bırakan uç sahtelenebilir (ölçüldü 2026-09-07). Kapsanmış agregasyon ucu sorguyu bir workflow query adımından çağırır: platform_guide('microservice') §'Kimlik kapsaması × uç kaynağı'. Ucun gerçek bir tuzağı varsa (auth istemiyor, sayfalama yok, atomik değil) 'caution' alanına yazın — OPSİYONELDİR, tuzak yoksa boş bırakın. Gövde politika kontrolünden geçer. " + constraintSummary('CreateMicroserviceCommand'),
|
|
291
|
-
inputSchema: { project: projectParam, environment: environmentParam, applicationId: appRef, definition: definitionParam, policyOverride: policyOverrideParam },
|
|
294
|
+
description: "Yeni microservice tanımlar (definition = CreateMicroserviceCommand). Aynı ad ya da slug'da uç varsa varsayılan olarak YAZMAZ ve mevcut ucun id'sini döner (MS-023 — gönderilseydi platform '<ad> (1)' adıyla ikinci bir uç açardı); tekrar çalıştırılabilir yazma için ifExists:'update' verin, yanıttaki outcome 'created' | 'updated' olur. Mevcut uçlar okunamazsa da yazmaz. İlk kullanımdan önce platform_guide('microservice') okuyun: kaynak seçimi (table > query) ve selectFields disiplini. 'description' ZORUNLUDUR (MS-014, block): çağıranın bu uçtan ne elde ettiğini yazın. Auth/oturum gerektiren uçlarda definition.middlewareIds ile middleware bağlayın; kimlik kapsaması filtrede source:'expression' + value:'user.id' ile yapılır (platform_guide('middleware')). BU YALNIZ source:'table' UÇLARDA VAR: query kaynaklı ucun parametreleri ÇAĞRIDAN gelir (body → query-string → header) ve middleware'in kurduğu user o yola girmez — kapsamı gövdeye bırakan uç sahtelenebilir (ölçüldü 2026-09-07). Kapsanmış agregasyon ucu sorguyu bir workflow query adımından çağırır: platform_guide('microservice') §'Kimlik kapsaması × uç kaynağı'. Ucun gerçek bir tuzağı varsa (auth istemiyor, sayfalama yok, atomik değil) 'caution' alanına yazın — OPSİYONELDİR, tuzak yoksa boş bırakın. Gövde politika kontrolünden geçer. " + constraintSummary('CreateMicroserviceCommand'),
|
|
295
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: appRef, definition: definitionParam, ifExists: microserviceIfExistsParam, view: recordWriteViewParam, policyOverride: policyOverrideParam },
|
|
292
296
|
annotations: writes
|
|
293
|
-
}, handle(config, async ({ project, environment, applicationId, definition, policyOverride }) => {
|
|
297
|
+
}, handle(config, async ({ project, environment, applicationId, definition, ifExists, view, policyOverride }) => {
|
|
294
298
|
const resolver = new RefResolver(platform, project, environment);
|
|
295
299
|
const resolvedApplicationId = await resolver.applicationId(applicationId);
|
|
296
300
|
const hydrated = await hydrateMicroserviceRefs(definition, resolver);
|
|
297
301
|
const normalized = normalizeMicroserviceDefinition(definition);
|
|
302
|
+
// Politika ÖNCE koşar: block alan istek platforma hiç gitmez, var-mı listesi de okunmaz.
|
|
303
|
+
const createFindings = enforcePolicy(evaluatePolicy(config, policyRules, 'microservice', definition, { operation: 'create' }, policyOverride));
|
|
304
|
+
const scopeFindings = await queryScopeFindings(platform, project, environment, definition);
|
|
305
|
+
const existing = await existingMatch({
|
|
306
|
+
code: 'MS-023',
|
|
307
|
+
noun: 'uç',
|
|
308
|
+
where: 'bu uygulamada',
|
|
309
|
+
name: typeof definition.name === 'string' ? definition.name : undefined,
|
|
310
|
+
slug: typeof definition.slug === 'string' ? definition.slug : undefined,
|
|
311
|
+
id: typeof definition.id === 'string' ? definition.id : undefined,
|
|
312
|
+
idParam: 'definition.id',
|
|
313
|
+
ifExists,
|
|
314
|
+
list: () => listAll(platform, project, environment, `/microservices/${resolvedApplicationId}`),
|
|
315
|
+
listTool: 'microservice_list',
|
|
316
|
+
copyCheck: 'kullanılıp kullanılmadığını dependency_graph ile kontrol edip gerekirse silin',
|
|
317
|
+
updateTool: 'id ile microservice_update'
|
|
318
|
+
});
|
|
319
|
+
// Kayıt bulunduysa yazma bir UPDATE'tir: gövde kuralları microservice_update ile aynı kipte (R-1).
|
|
298
320
|
const findings = [
|
|
299
|
-
...
|
|
300
|
-
|
|
321
|
+
...(existing === undefined
|
|
322
|
+
? createFindings
|
|
323
|
+
: enforcePolicy(evaluatePolicy(config, policyRules, 'microservice', definition, { operation: 'update' }, policyOverride))),
|
|
324
|
+
...scopeFindings
|
|
301
325
|
];
|
|
302
|
-
const drift = unmappedMembers('CreateMicroserviceCommand', definition);
|
|
303
|
-
const payload =
|
|
304
|
-
|
|
326
|
+
const drift = unmappedMembers(existing === undefined ? 'CreateMicroserviceCommand' : 'UpdateMicroserviceCommand', definition);
|
|
327
|
+
const payload = existing === undefined
|
|
328
|
+
? await platform.tenant('POST', project, `/microservices/${resolvedApplicationId}`, { environment, body: definition })
|
|
329
|
+
: await platform.tenant('PUT', project, `/microservices/${resolvedApplicationId}/${String(existing.id)}`, { environment, body: definition });
|
|
330
|
+
const saved = payload?.data;
|
|
331
|
+
const rename = ifExists === 'duplicate' ? platformRename(definition, saved, 'dash') : { sent: definition };
|
|
332
|
+
return attachFindings(withUnmappedMembers(withWriteDiff(withResolved(withRename(withNormalization(withOutcome(outlineWrite(payload, view, MICROSERVICE_OUTLINE_KEYS), existing), normalized), rename.renamed), [...resolver.resolved, ...hydrated]), writeDiff(rename.sent, payload)), drift), findings);
|
|
305
333
|
}));
|
|
306
334
|
server.registerTool('microservice_update', {
|
|
307
335
|
title: 'Microservice güncelle',
|
|
308
336
|
description: 'Microservice tanımını günceller (tam gövde). Hedefli değişiklik için microservice_patch tercih edin. \'description\' ZORUNLUDUR (MS-014, block). Ucun gerçek bir tuzağı varsa (auth istemiyor, sayfalama yok, atomik değil) \'caution\' alanına yazın — OPSİYONELDİR, tuzak yoksa boş bırakın. Gövde politika kontrolünden geçer. ' + constraintSummary('UpdateMicroserviceCommand'),
|
|
309
|
-
inputSchema: { project: projectParam, environment: environmentParam, applicationId: appRef, id: guid, definition: definitionParam, policyOverride: policyOverrideParam },
|
|
337
|
+
inputSchema: { project: projectParam, environment: environmentParam, applicationId: appRef, id: guid, definition: definitionParam, view: recordWriteViewParam, policyOverride: policyOverrideParam },
|
|
310
338
|
annotations: updates
|
|
311
|
-
}, handle(config, async ({ project, environment, applicationId, id, definition, policyOverride }) => {
|
|
339
|
+
}, handle(config, async ({ project, environment, applicationId, id, definition, view, policyOverride }) => {
|
|
312
340
|
const resolver = new RefResolver(platform, project, environment);
|
|
313
341
|
const resolvedApplicationId = await resolver.applicationId(applicationId);
|
|
314
342
|
const hydrated = await hydrateMicroserviceRefs(definition, resolver);
|
|
@@ -319,7 +347,7 @@ export function registerMicroserviceTools(server, platform) {
|
|
|
319
347
|
];
|
|
320
348
|
const drift = unmappedMembers('UpdateMicroserviceCommand', definition);
|
|
321
349
|
const payload = await platform.tenant('PUT', project, `/microservices/${resolvedApplicationId}/${id}`, { environment, body: definition });
|
|
322
|
-
return attachFindings(withUnmappedMembers(withWriteDiff(withResolved(withNormalization(payload, normalized), [...resolver.resolved, ...hydrated]), writeDiff(definition, payload)), drift), findings);
|
|
350
|
+
return attachFindings(withUnmappedMembers(withWriteDiff(withResolved(withNormalization(outlineWrite(payload, view, MICROSERVICE_OUTLINE_KEYS), normalized), [...resolver.resolved, ...hydrated]), writeDiff(definition, payload)), drift), findings);
|
|
323
351
|
}));
|
|
324
352
|
server.registerTool('microservice_patch', {
|
|
325
353
|
title: 'Microservice\'e patch uygula',
|
package/dist/tools/pages.js
CHANGED
|
@@ -6,8 +6,9 @@ import { fetchComponentEventNames } from './components.js';
|
|
|
6
6
|
import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
|
|
7
7
|
import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, planEventRemovals, resizeServerOps, planResizes, planPropTypes, rejectedOpsMessage } from './schemaOps.js';
|
|
8
8
|
import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
|
|
9
|
-
import {
|
|
9
|
+
import { emptyWriteDiff, schemaOpDrops, unwrapProps, withWriteDiff, writeDiff } from '../platform/writeDiff.js';
|
|
10
10
|
import { PlatformApiError } from '../platform/errors.js';
|
|
11
|
+
import { existingMatch, ifExistsParam, listAll, withOutcome } from '../platform/ifExists.js';
|
|
11
12
|
import { handle, toolResult, toolError } from '../platform/responses.js';
|
|
12
13
|
import { StudioScreenshotter } from '../platform/screenshot.js';
|
|
13
14
|
import { diffSummaries, summaryLines, summaryStats } from '../platform/domSummary.js';
|
|
@@ -55,6 +56,10 @@ async function orderByIndex(ops, readPage) {
|
|
|
55
56
|
return { ops, moved: [], warnings: [] };
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
/** Şema ağacındaki TÜM düğümler (kök + iç içe çocuklar); page_get ve page_create özeti aynı sayımı kullanır. */
|
|
60
|
+
function countSchemaNodes(nodes) {
|
|
61
|
+
return (nodes ?? []).reduce((total, node) => total + 1 + countSchemaNodes(node.children), 0);
|
|
62
|
+
}
|
|
58
63
|
async function fetchPage(platform, project, environment, applicationId, pageId) {
|
|
59
64
|
const response = await platform.tenant('GET', project, `/pages/${applicationId}/${pageId}`, { environment });
|
|
60
65
|
return response?.data ?? {};
|
|
@@ -101,7 +106,6 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
101
106
|
}, handle(config, async ({ project, environment, applicationId, pageId }) => {
|
|
102
107
|
const page = await fetchPage(platform, project, environment, applicationId, pageId);
|
|
103
108
|
const layout = (page.layout ?? {});
|
|
104
|
-
const countNodes = (nodes) => (nodes ?? []).reduce((total, node) => total + 1 + countNodes(node.children), 0);
|
|
105
109
|
return {
|
|
106
110
|
id: page.id,
|
|
107
111
|
name: page.name,
|
|
@@ -118,7 +122,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
118
122
|
events: page.events ?? [],
|
|
119
123
|
flows: (Array.isArray(page.flows) ? page.flows : [])
|
|
120
124
|
.map(flow => ({ id: flow.id, name: flow.name, slug: flow.slug })),
|
|
121
|
-
schemaNodes:
|
|
125
|
+
schemaNodes: countSchemaNodes(page.schema),
|
|
122
126
|
updatedAt: page.audit?.updatedAt ?? page.audit?.createdAt
|
|
123
127
|
};
|
|
124
128
|
}));
|
|
@@ -200,7 +204,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
200
204
|
const maxInlinePngBytes = 5 * 1024 * 1024;
|
|
201
205
|
server.registerTool('page_screenshot', {
|
|
202
206
|
title: 'Sayfanın ekran görüntüsünü al',
|
|
203
|
-
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).",
|
|
207
|
+
description: "ÖNCE KARESİZ DENE: 'kart göründü mü, tablo doldu mu, hata var mı' gibi olgu sorularında image:'none' verin — kare çekilmez, yanıttaki summaryText her kök düğümün görünürlüğünü, metnini, satır sayısını, açık sekmesini ve hata mesajlarını söyler (bir tam sayfa karesinin yüzlerce katı küçük). Kareyi yalnız görsel soru (renk, hizalama, referansa sadakat) için alın ve küçültün: clip ile bölge, scale:0.5 ile yarı çözünürlük; aynı sayfayı yeniden çekerken image:'ifChanged'. Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve Studio'nun çizdiği hata durumu karesi döner — bu da bir doğrulamadır. YÜKLENMEMİŞ SAYFA ASLA ÇEKİLMEZ: kare, runtime çizilip mount akışları bitince alınır (yanıtta pageState + readyAfterMs); bütçe dolduğunda sayfa hâlâ iskelette ya da akış koşuyorsa çağrı ne görüldüğünü söyleyen bir hatayla döner, boş/yarım kare dönmez. Sayfa akışı bir adımda düşerse (patlayan ifade, hata dönen uç) yanıt runtimeErrors taşır ve hangi komponentin hangi prop'unun YAZILMADIĞINI söyler — kare 'çalışıyor' görünürken sessizce boş kalan komponenti ancak bu yakalar. Kare DİLİ ve TEMASI seçilebilir: 'language' adrese ?lang ekler (çeviriye bağlı prop'lar da çok dilli alanların satır değerleri de o dilde çözülür), 'colorScheme' ziyaretçinin tema tercihini sayfa açılmadan eker — ikisi de sayfaya dil değiştirici ya da theme-toggle koymayı GEREKTİRMEZ, yani referansa sadık bir tasarımın ikinci dili ve koyu teması bozulmadan kanıtlanır. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir. DAL ORTAMI DA GÖRÜNTÜLENİR (2026-09-09'dan beri): 'development' /dev, 'production' /view, dal ise /branch/{dal} şeridinden açılır ve sayfanın BÜTÜN istekleri (şema da akışlar da) o dala gider — dalda kurulan sayfa artık merge'i beklemeden gözle doğrulanır (platform_guide('merge') §Dalda görsel doğrulama).",
|
|
204
208
|
inputSchema: {
|
|
205
209
|
project: projectParam,
|
|
206
210
|
environment: environmentParam,
|
|
@@ -787,14 +791,14 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
787
791
|
// alanın gerçekten KAYDEDİLDİĞİNİ söylemez. Yamadan sonra şemayı bir kez okuyup op'un yazmak
|
|
788
792
|
// istediğiyle kayıtlı olanı karşılaştırıyoruz. Okuma başarısız olursa yama yine geçerlidir.
|
|
789
793
|
const contentOps = sentOps.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
|
|
790
|
-
let opDrops =
|
|
794
|
+
let opDrops = emptyWriteDiff();
|
|
791
795
|
if (contentOps.length > 0) {
|
|
792
796
|
try {
|
|
793
797
|
const saved = await fetchPage(platform, project, environment, applicationId, pageId);
|
|
794
798
|
opDrops = schemaOpDrops(saved.schema, contentOps);
|
|
795
799
|
}
|
|
796
800
|
catch {
|
|
797
|
-
opDrops =
|
|
801
|
+
opDrops = emptyWriteDiff();
|
|
798
802
|
}
|
|
799
803
|
}
|
|
800
804
|
return withWriteDiff(withNormalization(response, [
|
|
@@ -822,7 +826,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
822
826
|
})));
|
|
823
827
|
server.registerTool('page_create', {
|
|
824
828
|
title: 'Sayfa oluştur',
|
|
825
|
-
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.",
|
|
829
|
+
description: "Yeni sayfa açar. Uygulamada aynı ad ya da slug'da sayfa varsa varsayılan olarak YAZMAZ ve mevcut sayfanın id'sini döner (PAGE-035 — gönderilseydi platform '<ad> (1)' adıyla ikinci bir sayfa açardı); tekrar çalıştırılabilir yazma için ifExists:'update' verin, yanıttaki outcome 'created' | 'updated' olur. Mevcut sayfalar okunamazsa da yazmaz. Ş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.",
|
|
826
830
|
inputSchema: {
|
|
827
831
|
project: projectParam,
|
|
828
832
|
environment: environmentParam,
|
|
@@ -836,10 +840,11 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
836
840
|
middlewareIds: z.array(guid).optional(),
|
|
837
841
|
events: eventsParam.optional(),
|
|
838
842
|
schema: schemaParam.optional(),
|
|
839
|
-
view: writeViewParam
|
|
843
|
+
view: writeViewParam,
|
|
844
|
+
ifExists: ifExistsParam('sayfa', "mevcut sayfayı page_update gibi günceller — yalnız bu çağrıda VERİLEN alanlar yazılır, verilmeyen şema/olaylar/roller korunur (page_schema_patch ile kurulmuş şema betik tekrar koşunca silinmez); id'si ve flow'ları korunur")
|
|
840
845
|
},
|
|
841
846
|
annotations: writes
|
|
842
|
-
}, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, events, schema, view }) => {
|
|
847
|
+
}, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, events, schema, view, ifExists }) => {
|
|
843
848
|
const container = ensurePageContainer(schema ?? []);
|
|
844
849
|
assertScreenWriteGuards('page', { schema: container.schema, events });
|
|
845
850
|
const stringStyles = stringStyleNodes(container.schema);
|
|
@@ -850,6 +855,44 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
850
855
|
: [];
|
|
851
856
|
const moved = reorderSchemaBlocks(container.schema);
|
|
852
857
|
const gaps = (schema ?? []).length > 0 ? verticalGapWarnings(container.schema, 'PAGE-010 (uyarı)') : [];
|
|
858
|
+
const existing = await existingMatch({
|
|
859
|
+
code: 'PAGE-035',
|
|
860
|
+
noun: 'sayfa',
|
|
861
|
+
where: 'bu uygulamada',
|
|
862
|
+
name,
|
|
863
|
+
slug,
|
|
864
|
+
idParam: 'pageId',
|
|
865
|
+
ifExists,
|
|
866
|
+
list: () => listAll(platform, project, environment, `/pages/${applicationId}`),
|
|
867
|
+
listTool: 'page_list',
|
|
868
|
+
copyCheck: "kullanılıp kullanılmadığını dependency_graph module:'page' ile kontrol edip gerekirse silin",
|
|
869
|
+
updateTool: 'pageId ile page_update'
|
|
870
|
+
});
|
|
871
|
+
if (existing !== undefined) {
|
|
872
|
+
const pageId = String(existing.id);
|
|
873
|
+
const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
|
|
874
|
+
const overrides = Object.fromEntries(Object.entries({
|
|
875
|
+
name, slug, browserTitle, browserTitleKey, layoutId, roleIds, middlewareIds, events,
|
|
876
|
+
schema: schema === undefined ? undefined : container.schema
|
|
877
|
+
}).filter(([, value]) => value !== undefined));
|
|
878
|
+
const body = pageUpdateBody(current, overrides);
|
|
879
|
+
const before = pageUpdateBody(current);
|
|
880
|
+
// Aynı ad/slug ile tekrar koşan betik o alanları "değişti" diye görmemeli (Gözlem 2).
|
|
881
|
+
const changed = Object.keys(overrides).filter(key => JSON.stringify(before[key]) !== JSON.stringify(overrides[key]));
|
|
882
|
+
await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
|
|
883
|
+
const updateWarnings = schema === undefined
|
|
884
|
+
? []
|
|
885
|
+
: [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema)), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'), ...spacingLayerWarnings(container.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')];
|
|
886
|
+
const summary = {
|
|
887
|
+
id: pageId,
|
|
888
|
+
outcome: 'updated',
|
|
889
|
+
changed,
|
|
890
|
+
preserved: { schemaNodes: countSchemaNodes(body.schema), events: body.events.length }
|
|
891
|
+
};
|
|
892
|
+
if (updateWarnings.length > 0)
|
|
893
|
+
summary.layoutWarnings = updateWarnings;
|
|
894
|
+
return withNormalization(summary, schema === undefined ? [] : [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]);
|
|
895
|
+
}
|
|
853
896
|
const response = await platform.tenant('POST', project, `/pages/${applicationId}`, {
|
|
854
897
|
environment,
|
|
855
898
|
body: {
|
|
@@ -871,12 +914,12 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
871
914
|
// kaydedildi mi" sorusunu gövde değil bu diff cevaplar. `layout_create`'in yaptığının sayfa
|
|
872
915
|
// ikizi; Prop sarmalı açılmazsa doğru kaydedilmiş her prop düşmüş görünür (G-28).
|
|
873
916
|
const drops = (schema ?? []).length > 0
|
|
874
|
-
?
|
|
875
|
-
:
|
|
917
|
+
? writeDiff({ schema: unwrapProps(container.schema) }, { schema: unwrapProps(saved.schema) })
|
|
918
|
+
: emptyWriteDiff();
|
|
876
919
|
if (view !== 'full' && Array.isArray(saved.schema)) {
|
|
877
920
|
saved.schema = saved.schema.map(node => summarize(node));
|
|
878
921
|
}
|
|
879
|
-
const normalized = withWriteDiff(withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), drops);
|
|
922
|
+
const normalized = withWriteDiff(withNormalization(withOutcome(response, undefined), [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved)]), drops);
|
|
880
923
|
const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema)), ...flexSizeWarnings(container.schema, 'PAGE-017 (uyarı)', 'PAGE-018 (uyarı)'), ...spacingLayerWarnings(container.schema, 'PAGE-029 (uyarı)', 'PAGE-030 (uyarı)')];
|
|
881
924
|
if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
|
|
882
925
|
return normalized;
|