@appaflytech/zeyrek-ai-mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.env.example +46 -0
  2. package/README.md +260 -0
  3. package/dist/config.js +128 -0
  4. package/dist/index.js +88 -0
  5. package/dist/knowledge/componentCatalog.js +438 -0
  6. package/dist/knowledge/componentEvents.js +164 -0
  7. package/dist/knowledge/coverage.js +60 -0
  8. package/dist/knowledge/normalize.js +1309 -0
  9. package/dist/knowledge/outlines.js +167 -0
  10. package/dist/knowledge/policy.js +44 -0
  11. package/dist/knowledge/reservedTables.js +75 -0
  12. package/dist/knowledge/rules.js +1725 -0
  13. package/dist/observability/trace.js +337 -0
  14. package/dist/platform/errors.js +132 -0
  15. package/dist/platform/http.js +169 -0
  16. package/dist/platform/responses.js +94 -0
  17. package/dist/platform/token.js +79 -0
  18. package/dist/resources/examples/entity-pair-m2m.json +105 -0
  19. package/dist/resources/examples/flow-list-to-form.json +65 -0
  20. package/dist/resources/examples/flow-load-table.json +178 -0
  21. package/dist/resources/examples/microservice-table-list.json +49 -0
  22. package/dist/resources/examples/middleware-authenticate.json +194 -0
  23. package/dist/resources/examples/page-detail-profile.json +95 -0
  24. package/dist/resources/examples/page-dynamic-form.json +221 -0
  25. package/dist/resources/examples/page-form-upsert.json +279 -0
  26. package/dist/resources/examples/page-html-embed.json +106 -0
  27. package/dist/resources/examples/page-sign-in.json +194 -0
  28. package/dist/resources/examples/workflow-basic.json +94 -0
  29. package/dist/resources/guides/component.md +108 -0
  30. package/dist/resources/guides/entity.md +348 -0
  31. package/dist/resources/guides/expressions.md +139 -0
  32. package/dist/resources/guides/getting-started.md +109 -0
  33. package/dist/resources/guides/html.md +106 -0
  34. package/dist/resources/guides/microservice.md +313 -0
  35. package/dist/resources/guides/middleware.md +271 -0
  36. package/dist/resources/guides/modify-existing.md +35 -0
  37. package/dist/resources/guides/page.md +1574 -0
  38. package/dist/resources/guides/view.md +89 -0
  39. package/dist/resources/guides/workflow.md +359 -0
  40. package/dist/resources/knowledge.js +44 -0
  41. package/dist/resources/schemas/CreateDatasourceCommand.json +54 -0
  42. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +133 -0
  43. package/dist/resources/schemas/CreateEntityCommand.json +387 -0
  44. package/dist/resources/schemas/CreateMicroserviceCommand.json +528 -0
  45. package/dist/resources/schemas/CreateQueryCommand.json +43 -0
  46. package/dist/resources/schemas/CreateViewCommand.json +158 -0
  47. package/dist/resources/schemas/CreateWorkflowCommand.json +1719 -0
  48. package/dist/resources/schemas/Filter.json +124 -0
  49. package/dist/resources/schemas/GetRowsQuery.json +122 -0
  50. package/dist/resources/schemas/PreviewMicroserviceCommand.json +565 -0
  51. package/dist/resources/schemas/PreviewQueryCommand.json +74 -0
  52. package/dist/resources/schemas/PreviewWorkflowCommand.json +981 -0
  53. package/dist/resources/schemas/StepDto.json +900 -0
  54. package/dist/resources/schemas/UpdateDatasourceCommand.json +58 -0
  55. package/dist/resources/schemas/UpdateEntityCommand.json +679 -0
  56. package/dist/resources/schemas/UpdateMicroserviceCommand.json +512 -0
  57. package/dist/resources/schemas/UpdateQueryCommand.json +86 -0
  58. package/dist/resources/schemas/UpdateViewCommand.json +153 -0
  59. package/dist/resources/schemas/UpdateWorkflowCommand.json +1719 -0
  60. package/dist/resources/schemas.js +40 -0
  61. package/dist/server.js +63 -0
  62. package/dist/tools/access.js +27 -0
  63. package/dist/tools/applications.js +59 -0
  64. package/dist/tools/automations.js +103 -0
  65. package/dist/tools/common.js +18 -0
  66. package/dist/tools/components.js +280 -0
  67. package/dist/tools/datasources.js +82 -0
  68. package/dist/tools/dependencies.js +17 -0
  69. package/dist/tools/deployments.js +17 -0
  70. package/dist/tools/dtos.js +54 -0
  71. package/dist/tools/entities.js +166 -0
  72. package/dist/tools/files.js +138 -0
  73. package/dist/tools/flows.js +226 -0
  74. package/dist/tools/folders.js +51 -0
  75. package/dist/tools/layouts.js +105 -0
  76. package/dist/tools/microservices.js +198 -0
  77. package/dist/tools/organizations.js +65 -0
  78. package/dist/tools/pages.js +597 -0
  79. package/dist/tools/platformInfo.js +44 -0
  80. package/dist/tools/projects.js +152 -0
  81. package/dist/tools/queries.js +48 -0
  82. package/dist/tools/rows.js +115 -0
  83. package/dist/tools/screens.js +97 -0
  84. package/dist/tools/templates.js +65 -0
  85. package/dist/tools/variables.js +94 -0
  86. package/dist/tools/views.js +58 -0
  87. package/dist/tools/workflows.js +196 -0
  88. package/package.json +49 -0
@@ -0,0 +1,597 @@
1
+ import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
2
+ import { orphanedFlowWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, blockSizeNote, heightTypeNote, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, invalidScreenEventNames, invalidWidgetTypeNodes, withNormalization, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
3
+ import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap } from './screens.js';
4
+ import { applyCatalogLayoutRules, fetchComponentEventNames } from './components.js';
5
+ import { formBindingWarnings } from '../knowledge/coverage.js';
6
+ import { PlatformApiError } from '../platform/errors.js';
7
+ import { handle } from '../platform/responses.js';
8
+ import { z } from 'zod';
9
+ const opsParam = z.array(z.record(z.unknown())).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} | set_binding {target, prop, expression: '{{ Page.variables.x }}'} | add_component {parent (üst elementId ya da 'root'), index?, component} | remove_component {target} | move_component {target, parent, index?, position: {x, y}} | set_event {target, event, eventValue} | remove_event {target, event} (gateway emülasyonu: platformun micro-op'u olay SİLEMEZ, eventValue: null yalnız değersiz ölü kayıt bırakır — bu op şemayı okuyup olayı diziden çıkarır ve sayfayı tam gövdeyle geri yazar) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler). target = elementId.");
10
+ const modeParam = z.enum(['summary', 'subtree', 'full']).optional().describe("summary (varsayılan): yalnız id/elementId/name/type ağacı — hedef bulmak için bunu kullanın. subtree: componentId ile tek alt ağaç. full: tüm şema.");
11
+ /**
12
+ * PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
13
+ * komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
14
+ * tipini ve o tipin katalogdaki olay listesini pozitif olarak çözebildiğinde konuşur (layout/widget
15
+ * içindeki hedefler ve katalogda olay tanımlamayan tipler doğrulanmadan geçer).
16
+ */
17
+ function unknownEventOps(ops, typesByElementId, eventsBySlug) {
18
+ return ops
19
+ .map(op => op)
20
+ .filter(op => op.op === 'set_event')
21
+ .flatMap(op => {
22
+ const target = String(op.target ?? '');
23
+ const event = String(op.event ?? '');
24
+ const type = typesByElementId.get(target);
25
+ const declared = type ? eventsBySlug.get(type) : undefined;
26
+ if (!event || !declared || declared.includes(event))
27
+ return [];
28
+ return [`'${target}' (${type}) komponenti '${event}' diye bir olay YAYMAZ. Yaydıkları: ${declared.join(', ')}. Platform bu op'u 200 ile kabul eder ama bağ sessizce ölü kalır — akış hiçbir zaman çalışmaz. Doğru adı component_get(slug:'${type}') çıktısındaki events listesinden alın.`];
29
+ });
30
+ }
31
+ function collectTypes(nodes, into) {
32
+ for (const node of nodes ?? []) {
33
+ if (typeof node.elementId === 'string' && typeof node.type === 'string')
34
+ into.set(node.elementId, node.type);
35
+ collectTypes(node.children, into);
36
+ }
37
+ }
38
+ function invalidComponentNames(nodes) {
39
+ const found = [];
40
+ for (const node of nodes ?? []) {
41
+ const name = node.name;
42
+ if (typeof name === 'string' && name.length > 0 && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name.replace(/-/g, ''))) {
43
+ found.push(`'${name}' (${node.elementId ?? '?'})`);
44
+ }
45
+ found.push(...invalidComponentNames(node.children));
46
+ }
47
+ return found;
48
+ }
49
+ function summarize(node) {
50
+ const summary = {
51
+ id: node.id,
52
+ elementId: node.elementId,
53
+ name: node.name,
54
+ type: node.type
55
+ };
56
+ if (Array.isArray(node.children) && node.children.length > 0) {
57
+ summary.children = node.children.map(summarize);
58
+ }
59
+ return summary;
60
+ }
61
+ function findNode(nodes, target) {
62
+ for (const node of nodes ?? []) {
63
+ if (node.elementId === target || String(node.id) === target)
64
+ return node;
65
+ const found = findNode(node.children, target);
66
+ if (found)
67
+ return found;
68
+ }
69
+ return undefined;
70
+ }
71
+ function removeNodeFromTree(nodes, target) {
72
+ for (let index = 0; index < nodes.length; index++) {
73
+ if (nodes[index].elementId === target)
74
+ return nodes.splice(index, 1)[0];
75
+ const found = removeNodeFromTree(nodes[index].children ?? [], target);
76
+ if (found)
77
+ return found;
78
+ }
79
+ return undefined;
80
+ }
81
+ function setDesktopPosition(node, position) {
82
+ const blocks = Array.isArray(node.blocks) ? node.blocks : [];
83
+ const block = blocks.find(entry => Number(entry?.breakpoint) === 1024);
84
+ if (block)
85
+ block.position = { x: Number(position.x ?? 0), y: Number(position.y ?? 0) };
86
+ }
87
+ /**
88
+ * PAGE-010 boşluk analizi patch'in SONUCUNU görmek zorunda: tek başına op'lardaki y değerleri,
89
+ * kapsayıcının mevcut çocukları bilinmeden anlamsız. Op'lar okunan şemanın kopyasına uygulanır
90
+ * (add: parent'ın children'ına ekle — kökler retargetRootAdds ile page_container'a gittiği için
91
+ * burada da öyle çözülür; move: düğümü taşı ve verilen position'ı desktop block'una yaz) ve analiz
92
+ * yalnız dokunulan kapsayıcılarda koşar.
93
+ */
94
+ function simulateLayoutOps(schema, ops) {
95
+ const touched = new Set();
96
+ const attach = (node, requestedParent) => {
97
+ const parentId = typeof requestedParent === 'string' && requestedParent !== '' && requestedParent !== 'root'
98
+ ? requestedParent
99
+ : PAGE_CONTAINER_ELEMENT_ID;
100
+ const parent = findNode(schema, parentId);
101
+ if (parent) {
102
+ parent.children = [...(parent.children ?? []), node];
103
+ touched.add(String(parent.elementId ?? parentId));
104
+ }
105
+ else {
106
+ schema.push(node);
107
+ touched.add('kök');
108
+ }
109
+ };
110
+ for (const op of ops) {
111
+ if (op.op === 'add_component' && op.component && typeof op.component === 'object') {
112
+ attach(JSON.parse(JSON.stringify(op.component)), op.parent);
113
+ }
114
+ // Düzen prop'ları (layout/flex grubu) simülasyona girer: PAGE-010 flex kapsayıcının içini
115
+ // analiz etmez, PAGE-015 ise op'tan SONRAKİ layout değerine bakar — ikisi de op uygulanmadan
116
+ // doğru karar veremez.
117
+ if (op.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(op.prop ?? ''))) {
118
+ const target = findNode(schema, String(op.target ?? ''));
119
+ if (target)
120
+ target.props = { ...(target.props ?? {}), [String(op.prop)]: { type: 'static', ref: null, data: op.value } };
121
+ }
122
+ if (op.op === 'move_component') {
123
+ const moved = removeNodeFromTree(schema, String(op.target ?? ''));
124
+ if (!moved)
125
+ continue;
126
+ if (op.position && typeof op.position === 'object')
127
+ setDesktopPosition(moved, op.position);
128
+ attach(moved, op.parent);
129
+ }
130
+ }
131
+ return touched;
132
+ }
133
+ async function fetchPage(platform, project, environment, applicationId, pageId) {
134
+ const response = await platform.tenant('GET', project, `/pages/${applicationId}/${pageId}`, { environment });
135
+ return response?.data ?? {};
136
+ }
137
+ /**
138
+ * F-8 — platformun `set_event` micro-op'u olay KALDIRAMAZ: `eventValue: null` verildiğinde
139
+ * PageSchemaMicroOpApplier kaydı `{ name: 'click' }` olarak (değersiz) bırakır, diziden çıkarmaz;
140
+ * ölü bağ şemada artık olarak kalır. Kaldırma yolu tek: şemayı okuyup diziden çıkarmak ve sayfayı
141
+ * tam gövdeyle geri yazmak (page_update'in kullandığı PUT — pageUpdateBody her alanı korur).
142
+ * `target: 'page'` sayfa düzeyi olayları hedefler.
143
+ */
144
+ async function applyEventRemovals(platform, project, environment, applicationId, pageId, page, ops) {
145
+ const schema = JSON.parse(JSON.stringify(page.schema ?? []));
146
+ const pageEvents = Array.isArray(page.events) ? [...page.events] : [];
147
+ const removed = [];
148
+ let pageEventsTouched = false;
149
+ for (const op of ops) {
150
+ const target = String(op.target ?? '');
151
+ const event = String(op.event ?? '');
152
+ if (!target || !event)
153
+ throw new PlatformApiError("remove_event için 'target' ve 'event' zorunludur.", 400);
154
+ if (target === 'page') {
155
+ const before = pageEvents.length;
156
+ const kept = pageEvents.filter(entry => String(entry?.name ?? '') !== event);
157
+ if (kept.length !== before) {
158
+ pageEvents.length = 0;
159
+ pageEvents.push(...kept);
160
+ pageEventsTouched = true;
161
+ removed.push(`page.${event}`);
162
+ }
163
+ continue;
164
+ }
165
+ const node = findNode(schema, target);
166
+ if (!node)
167
+ throw new PlatformApiError(`remove_event hedefi bulunamadı: '${target}'. page_schema_get ile elementId'leri listeleyin.`, 400);
168
+ const events = Array.isArray(node.events) ? node.events : [];
169
+ const kept = events.filter(entry => String(entry?.name ?? '') !== event);
170
+ if (kept.length !== events.length)
171
+ removed.push(`${target}.${event}`);
172
+ node.events = kept;
173
+ }
174
+ if (removed.length === 0)
175
+ return [];
176
+ const body = pageUpdateBody(page, pageEventsTouched ? { schema, events: pageEvents } : { schema });
177
+ await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
178
+ return removed;
179
+ }
180
+ export function registerPageTools(server, platform) {
181
+ const config = platform.config;
182
+ server.registerTool('page_list', {
183
+ title: 'Sayfaları listele',
184
+ description: 'Uygulamanın sayfalarını (id, name, slug) listeler; hedef sayfayı bulmak için ilk adım.',
185
+ inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, ...pagingParams },
186
+ annotations: readOnly
187
+ }, handle(config, async ({ project, environment, applicationId, pageIndex, pageLength, order, isDescending }) => {
188
+ const response = await platform.tenant('GET', project, `/pages/${applicationId}`, {
189
+ environment,
190
+ query: { pageIndex: pageIndex ?? 0, pageLength: pageLength ?? 200, order, isDescending }
191
+ });
192
+ return {
193
+ pages: (response?.data ?? []).map(page => ({
194
+ id: page.id,
195
+ name: page.name,
196
+ slug: page.slug,
197
+ isHomepage: page.isHomepage
198
+ }))
199
+ };
200
+ }));
201
+ server.registerTool('page_get', {
202
+ title: 'Sayfa üstbilgisini getir',
203
+ description: "Sayfanın kimlik ve bağ bilgisi: slug, browserTitle, layoutId, middlewareIds, roleIds, sayfa düzeyi olaylar (events[].value = flow id) ve flow özetleri. ŞEMA DÖNDÜRMEZ — şema için page_schema_get. 'Sayfa middleware'e bağlı mı, hangi roller görüyor, hangi olaya hangi akış bağlı' sorularının cevabı budur (Kural 8 denetimi).",
204
+ inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
205
+ annotations: readOnly
206
+ }, handle(config, async ({ project, environment, applicationId, pageId }) => {
207
+ const page = await fetchPage(platform, project, environment, applicationId, pageId);
208
+ const layout = (page.layout ?? {});
209
+ const countNodes = (nodes) => (nodes ?? []).reduce((total, node) => total + 1 + countNodes(node.children), 0);
210
+ return {
211
+ id: page.id,
212
+ name: page.name,
213
+ slug: page.slug,
214
+ browserTitle: page.browserTitle ?? '',
215
+ isHomepage: page.isHomepage === true,
216
+ layoutId: page.layoutId ?? layout.id ?? null,
217
+ layoutName: layout.name,
218
+ middlewareIds: page.middlewareIds ?? [],
219
+ roleIds: Array.isArray(page.roleIds) && page.roleIds.length > 0
220
+ ? page.roleIds
221
+ : (Array.isArray(page.roles) ? page.roles.map(role => role.id) : []),
222
+ events: page.events ?? [],
223
+ flows: (Array.isArray(page.flows) ? page.flows : [])
224
+ .map(flow => ({ id: flow.id, name: flow.name, slug: flow.slug })),
225
+ schemaNodes: countNodes(page.schema),
226
+ updatedAt: page.audit?.updatedAt ?? page.audit?.createdAt
227
+ };
228
+ }));
229
+ server.registerTool('page_runtime_get', {
230
+ title: 'Sayfanın runtime çıktısını getir',
231
+ description: "Çıkan uygulamanın sayfa için aldığı GERÇEK paketi döner (GET /{project}/{application}/p/{slug} — sayfanın ham serve yolu budur; Studio preview ve runtime aynı ucu tüketir): sayfa şeması + layout views'ü + sayfa olayları + flow'lar + değişken anahtarları. Kurduğun sayfanın SON hâlini doğrulamanın yolu budur (layout-slot yerleşimi dahil — Kural 1f). DİKKAT: sayfaya bağlı middleware'ler bu çağrıda GERÇEKTEN koşar; middleware reddederse yanıt middlewareBlocked ile döner — korumalı sayfada bu beklenen davranıştır ve kendisi bir doğrulamadır.",
232
+ inputSchema: {
233
+ project: projectParam,
234
+ environment: environmentParam,
235
+ applicationSlug: z.string().min(1).describe("Uygulamanın slug'ı (application_list / project_overview)"),
236
+ pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
237
+ 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'),
238
+ 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.")
239
+ },
240
+ annotations: readOnly
241
+ }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, anonymous }) => {
242
+ let response;
243
+ try {
244
+ response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous });
245
+ }
246
+ catch (error) {
247
+ if (error instanceof PlatformApiError && typeof error.status === 'number' && error.status >= 300) {
248
+ return {
249
+ middlewareBlocked: true,
250
+ status: error.status,
251
+ message: error.message,
252
+ note: "Sayfaya bağlı middleware isteği reddetti ya da kayıt bulunamadı. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır. Sayfanın bağlarını page_get ile, middleware tarifini platform_guide('middleware') ile kontrol edin; slug'ları page_list/application_list doğrular."
253
+ };
254
+ }
255
+ throw error;
256
+ }
257
+ if (view === 'full')
258
+ return response;
259
+ const body = (response?.data ?? {});
260
+ const layout = body.layout;
261
+ const asNodes = (value) => (Array.isArray(value) ? value : []);
262
+ const named = (value) => (Array.isArray(value) ? value : []).map(entry => ({ id: entry.id, name: entry.name, slug: entry.slug }));
263
+ return {
264
+ page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, isHomepage: body.isHomepage },
265
+ events: body.events ?? [],
266
+ layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(summarize) } : null,
267
+ schema: asNodes(body.schema).map(summarize),
268
+ widgets: named(body.widgets),
269
+ flows: named(body.flows),
270
+ variableKeys: Object.keys((body.variables ?? {})),
271
+ hasTheme: Boolean(body.project?.theme),
272
+ note: "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f); tam gövde gerekirse view:'full'."
273
+ };
274
+ }));
275
+ server.registerTool('page_schema_get', {
276
+ title: 'Sayfa şemasını oku',
277
+ description: "Sayfanın komponent şemasını okur; yanıttaki updatedAt değeri page_schema_patch'in expectedUpdatedAt kontrolünde kullanılır.",
278
+ inputSchema: {
279
+ project: projectParam,
280
+ environment: environmentParam,
281
+ applicationId: guid,
282
+ pageId: guid,
283
+ mode: modeParam,
284
+ componentId: z.string().optional().describe("subtree kipinde kök alınacak komponentin elementId'si")
285
+ },
286
+ annotations: readOnly
287
+ }, handle(config, async ({ project, environment, applicationId, pageId, mode, componentId }) => {
288
+ const page = await fetchPage(platform, project, environment, applicationId, pageId);
289
+ const updatedAt = page.audit?.updatedAt ?? page.audit?.createdAt;
290
+ if (mode === 'full')
291
+ return { updatedAt, schema: page.schema ?? [] };
292
+ if (mode === 'subtree') {
293
+ if (!componentId)
294
+ throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
295
+ const subtree = findNode(page.schema, componentId);
296
+ if (!subtree)
297
+ throw new PlatformApiError(`Sayfada '${componentId}' id'li bir komponent yok; page_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
298
+ return { updatedAt, schema: subtree };
299
+ }
300
+ return { updatedAt, schema: (page.schema ?? []).map(summarize) };
301
+ }));
302
+ server.registerTool('page_state', {
303
+ title: 'Sayfa durumunu getir',
304
+ description: 'Sayfanın state ağacı: page/app değişkenleri, sayfadaki komponentler, layout slotları ve microservice imzaları. Binding ifadelerini yalnız buradaki isimlere bağlayın — isim uydurmayın.',
305
+ inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
306
+ annotations: readOnly
307
+ }, handle(config, ({ project, environment, applicationId, pageId }) => platform.tenant('GET', project, `/pages/${applicationId}/${pageId}/ai-context`, { environment })));
308
+ server.registerTool('page_schema_validate', {
309
+ title: 'Sayfa şemasını doğrula (kuru çalıştırma)',
310
+ 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.",
311
+ inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional() },
312
+ annotations: readOnly
313
+ }, handle(config, async ({ project, environment, applicationId, pageId, ops }) => {
314
+ const opRecords = (ops ?? []).map(op => op);
315
+ const normalized = normalizeSchemaOps(opRecords);
316
+ const validationIssues = schemaOpValidationErrors(opRecords);
317
+ const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
318
+ environment,
319
+ body: { ops: opRecords }
320
+ })) ?? {});
321
+ if (validationIssues.length > 0)
322
+ response.validationWarnings = validationIssues.map(issue => `PAGE-016 — ${issue}`);
323
+ return withNormalization(response, [
324
+ ...componentDefaultsNote(normalized.components),
325
+ ...validationNote(normalized.validations),
326
+ ...replacePropNote(normalized.props)
327
+ ]);
328
+ }));
329
+ server.registerTool('page_schema_patch', {
330
+ title: 'Sayfa şemasına patch uygula',
331
+ 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.",
332
+ inputSchema: {
333
+ project: projectParam,
334
+ environment: environmentParam,
335
+ applicationId: guid,
336
+ pageId: guid,
337
+ ops: opsParam,
338
+ expectedUpdatedAt: z.string().optional().describe("page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz")
339
+ },
340
+ annotations: updates
341
+ }, handle(config, async ({ project, environment, applicationId, pageId, ops, expectedUpdatedAt }) => {
342
+ const allOps = ops.map(op => op);
343
+ const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
344
+ const opRecords = allOps.filter(entry => entry.op !== 'remove_event');
345
+ const containerOps = opRecords
346
+ .filter(entry => (entry.op === 'remove_component' || entry.op === 'move_component') && entry.target === PAGE_CONTAINER_ELEMENT_ID);
347
+ if (containerOps.length > 0) {
348
+ 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);
349
+ }
350
+ let cachedPage;
351
+ const readPage = async () => {
352
+ cachedPage ??= await fetchPage(platform, project, environment, applicationId, pageId);
353
+ return cachedPage;
354
+ };
355
+ if (expectedUpdatedAt) {
356
+ const current = await readPage();
357
+ const updatedAt = current.audit?.updatedAt ?? current.audit?.createdAt;
358
+ if (updatedAt !== expectedUpdatedAt) {
359
+ throw new PlatformApiError(`Sayfa siz okuduktan sonra değişmiş (updatedAt: ${updatedAt}). page_schema_get ile yeniden okuyup planı tazeleyin.`, 409);
360
+ }
361
+ }
362
+ const normalized = normalizeSchemaOps(opRecords);
363
+ const validationIssues = schemaOpValidationErrors(opRecords);
364
+ if (validationIssues.length > 0) {
365
+ throw new PlatformApiError(`PAGE-016 — validation gövdesi eksik (istek gönderilmedi): ${validationIssues.join(' · ')}. `
366
+ + "Çalışan biçim dört alanı da taşır: { type: 'required'|'email'|'regex'|'max-length'|'min-length'|'max-value'|'min-value'|'value-options', message: '<kullanıcıya görünen metin>', value: <eşik/desen/true>, isActive: true }.", 400);
367
+ }
368
+ const wantsRootAdd = opRecords.some(entry => entry.op === 'add_component' && (entry.parent === undefined || entry.parent === null || entry.parent === 'root'));
369
+ let retargeted = [];
370
+ if (wantsRootAdd) {
371
+ const page = await readPage();
372
+ if ((page.schema ?? []).some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID)) {
373
+ retargeted = retargetRootAdds(opRecords, PAGE_CONTAINER_ELEMENT_ID);
374
+ }
375
+ }
376
+ if (opRecords.some(entry => entry.op === 'set_event')) {
377
+ const typesByElementId = new Map();
378
+ try {
379
+ collectTypes((await readPage()).schema, typesByElementId);
380
+ }
381
+ catch {
382
+ typesByElementId.clear();
383
+ }
384
+ collectTypes(opRecords.map(entry => entry.component).filter(Boolean), typesByElementId);
385
+ const eventsBySlug = await fetchComponentEventNames(platform, project, environment, applicationId).catch(() => new Map());
386
+ const unknown = unknownEventOps(opRecords, typesByElementId, eventsBySlug);
387
+ if (unknown.length > 0)
388
+ throw new PlatformApiError(`PAGE-008 — geçersiz olay adı (istek gönderilmedi): ${unknown.join(' · ')}`, 400);
389
+ }
390
+ const added = opRecords
391
+ .filter(entry => entry.op === 'add_component')
392
+ .map(entry => entry.component);
393
+ const badWidgets = invalidWidgetTypeNodes(added, 'PAGE-012');
394
+ if (badWidgets.length > 0)
395
+ throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
396
+ const badNames = invalidComponentNames(added);
397
+ if (badNames.length > 0) {
398
+ throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badNames.join(' · ')}. `
399
+ + 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; boşluklu ya da geçersiz bir ad '
400
+ + 'SAYFADAKİ TÜM ifadeleri "Missing initializer in const declaration" ile öldürür (layout adları her sayfaya sızar). '
401
+ + 'name alanını elementId ile aynı yaz; okunabilir Türkçe başlık için props.title / props.text / props.label kullan.', 400);
402
+ }
403
+ const catalog = added.length > 0
404
+ ? await applyCatalogLayoutRules(platform, project, environment, applicationId, added)
405
+ : { clamped: [], heights: [] };
406
+ const formWarnings = formBindingWarnings(opRecords);
407
+ let flowWarnings = [];
408
+ let layoutWarnings = [];
409
+ if (opRecords.some(entry => entry.op === 'remove_component')) {
410
+ try {
411
+ const page = await readPage();
412
+ flowWarnings = orphanedFlowWarnings(page.schema, page.events, opRecords);
413
+ }
414
+ catch {
415
+ flowWarnings = [];
416
+ }
417
+ }
418
+ let nestedAfterOps = [];
419
+ const simulatable = opRecords.some(entry => ['add_component', 'move_component'].includes(String(entry.op))
420
+ || (entry.op === 'replace_prop' && FLEX_LAYOUT_PROP_NAMES.has(String(entry.prop ?? ''))));
421
+ if (simulatable) {
422
+ try {
423
+ const page = await readPage();
424
+ const simulated = JSON.parse(JSON.stringify(page.schema ?? []));
425
+ const touched = simulateLayoutOps(simulated, opRecords);
426
+ // PAGE-013 op'lar UYGULANDIKTAN SONRAKİ ağaca bakar: tek başına eklenen düğüm masum
427
+ // görünür, iç içeliği ancak hedef parent zinciri belli olunca anlarsın.
428
+ nestedAfterOps = nestedRepeaterNodes(simulated);
429
+ layoutWarnings = [
430
+ ...pageContainerPositionWarning(page.schema),
431
+ ...verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched),
432
+ ...stringStyleWarning(stringStyleNodes(simulated)),
433
+ ...inertFlexPropWarning(inertFlexPropNodes(simulated))
434
+ ];
435
+ }
436
+ catch {
437
+ layoutWarnings = [];
438
+ }
439
+ }
440
+ if (nestedAfterOps.length > 0)
441
+ throw new PlatformApiError(nestedRepeaterMessage(nestedAfterOps), 400);
442
+ const removedEvents = removeEventOps.length > 0
443
+ ? await applyEventRemovals(platform, project, environment, applicationId, pageId, await readPage(), removeEventOps)
444
+ : [];
445
+ if (removeEventOps.length > 0)
446
+ cachedPage = undefined;
447
+ const response = opRecords.length > 0
448
+ ? ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, { environment, body: { ops: opRecords } })) ?? {})
449
+ : { valid: true };
450
+ if (removedEvents.length > 0)
451
+ response.removedEvents = removedEvents;
452
+ if (formWarnings.length > 0)
453
+ response.formWarnings = formWarnings;
454
+ if (flowWarnings.length > 0)
455
+ response.flowWarnings = flowWarnings;
456
+ if (layoutWarnings.length > 0)
457
+ response.layoutWarnings = layoutWarnings;
458
+ return withNormalization(response, [
459
+ ...rootAddsNote(retargeted, PAGE_CONTAINER_ELEMENT_ID),
460
+ ...componentDefaultsNote(normalized.components),
461
+ ...validationNote(normalized.validations),
462
+ ...blockSizeNote(catalog.clamped),
463
+ ...heightTypeNote(catalog.heights),
464
+ ...replacePropNote(normalized.props)
465
+ ]);
466
+ }));
467
+ server.registerTool('page_create', {
468
+ title: 'Sayfa oluştur',
469
+ description: "Yeni sayfa açar. Şemayı burada göndermek yerine boş bırakıp page_schema_patch micro-op'larıyla kurmak daha doğrudur (doğrulama duvarı + canlı canvas orada). roleIds için role_list, layoutId için layout_list kullanın.",
470
+ inputSchema: {
471
+ project: projectParam,
472
+ environment: environmentParam,
473
+ applicationId: guid,
474
+ name: z.string().min(1),
475
+ slug: z.string().min(1).describe("URL yolu, örn. 'orders' ya da 'orders/detail'"),
476
+ browserTitle: z.string().optional(),
477
+ layoutId: guid.optional(),
478
+ roleIds: z.array(guid).optional().describe('Sayfayı görebilecek roller; boş = herkese açık'),
479
+ middlewareIds: z.array(guid).optional(),
480
+ events: eventsParam.optional(),
481
+ schema: schemaParam.optional()
482
+ },
483
+ annotations: writes
484
+ }, handle(config, async ({ project, environment, applicationId, name, slug, browserTitle, layoutId, roleIds, middlewareIds, events, schema }) => {
485
+ const badEvents = invalidScreenEventNames(events, 'PAGE-011');
486
+ if (badEvents.length > 0)
487
+ throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
488
+ const container = ensurePageContainer(schema ?? []);
489
+ const badWidgets = invalidWidgetTypeNodes(container.schema, 'PAGE-012');
490
+ if (badWidgets.length > 0)
491
+ throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
492
+ const nested = nestedRepeaterNodes(container.schema);
493
+ if (nested.length > 0)
494
+ throw new PlatformApiError(nestedRepeaterMessage(nested), 400);
495
+ const stringStyles = stringStyleNodes(container.schema);
496
+ const fixed = normalizeComponentDefaults(container.schema);
497
+ const reset = resetPageContainerPosition(container.schema);
498
+ const catalog = (schema ?? []).length > 0
499
+ ? await applyCatalogLayoutRules(platform, project, environment, applicationId, container.schema)
500
+ : { clamped: [], heights: [] };
501
+ const moved = reorderSchemaBlocks(container.schema);
502
+ const gaps = (schema ?? []).length > 0 ? verticalGapWarnings(container.schema, 'PAGE-010 (uyarı)') : [];
503
+ const response = await platform.tenant('POST', project, `/pages/${applicationId}`, {
504
+ environment,
505
+ body: {
506
+ name,
507
+ slug,
508
+ browserTitle: browserTitle ?? '',
509
+ layoutId: layoutId ?? null,
510
+ roleIds: roleIds ?? [],
511
+ middlewareIds: middlewareIds ?? [],
512
+ events: events ?? [],
513
+ schema: container.schema
514
+ }
515
+ });
516
+ const normalized = withNormalization(response, [...container.fixes, ...reset, ...componentDefaultsNote(fixed), ...blockSizeNote(catalog.clamped), ...heightTypeNote(catalog.heights), ...reorderNote(moved)]);
517
+ const createWarnings = [...gaps, ...stringStyleWarning(stringStyles), ...inertFlexPropWarning(inertFlexPropNodes(container.schema))];
518
+ if (createWarnings.length === 0 || !normalized || typeof normalized !== 'object')
519
+ return normalized;
520
+ return { ...normalized, layoutWarnings: createWarnings };
521
+ }));
522
+ server.registerTool('page_update', {
523
+ title: 'Sayfa üstbilgisini güncelle',
524
+ description: "Sayfanın adını/slug'ını/layout'unu/rollerini değiştirir. Platformun PUT'u REPLACE'tir; gateway sayfayı önce okur, yalnız verdiğiniz alanları değiştirip tam gövdeyi geri yazar — bu yüzden şema ve olaylar kaybolmaz. ŞEMA değişikliği için bu tool'u değil page_schema_patch'i kullanın (doğrulama duvarı + canlı canvas orada).",
525
+ inputSchema: {
526
+ project: projectParam,
527
+ environment: environmentParam,
528
+ applicationId: guid,
529
+ pageId: guid,
530
+ name: z.string().min(1).optional(),
531
+ slug: z.string().min(1).optional(),
532
+ browserTitle: z.string().optional(),
533
+ layoutId: guid.nullable().optional().describe('null: layout bağını kaldırır'),
534
+ roleIds: z.array(guid).optional(),
535
+ middlewareIds: z.array(guid).optional(),
536
+ events: eventsParam.optional(),
537
+ schema: schemaParam.optional().describe('Nadiren gerekir; verilmezse mevcut şema korunur')
538
+ },
539
+ annotations: updates
540
+ }, handle(config, async ({ project, environment, applicationId, pageId, ...changes }) => {
541
+ const overrides = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== undefined));
542
+ const badEvents = invalidScreenEventNames(overrides.events, 'PAGE-011');
543
+ if (badEvents.length > 0)
544
+ throw new PlatformApiError(`Geçersiz sayfa olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
545
+ const badWidgets = invalidWidgetTypeNodes(overrides.schema, 'PAGE-012');
546
+ if (badWidgets.length > 0)
547
+ throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
548
+ const nestedUpdate = nestedRepeaterNodes(overrides.schema);
549
+ if (nestedUpdate.length > 0)
550
+ throw new PlatformApiError(nestedRepeaterMessage(nestedUpdate), 400);
551
+ const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
552
+ const badSchemaNames = invalidComponentNames(overrides.schema);
553
+ if (badSchemaNames.length > 0) {
554
+ throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badSchemaNames.join(' · ')}. `
555
+ + 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; geçersiz bir ad sayfadaki TÜM ifadeleri öldürür. '
556
+ + 'name = elementId yazın.', 400);
557
+ }
558
+ const fixed = normalizeComponentDefaults(overrides.schema);
559
+ const reset = overrides.schema === undefined ? [] : resetPageContainerPosition(overrides.schema);
560
+ const catalog = overrides.schema === undefined
561
+ ? { clamped: [], heights: [] }
562
+ : await applyCatalogLayoutRules(platform, project, environment, applicationId, overrides.schema);
563
+ const moved = overrides.schema === undefined ? [] : reorderSchemaBlocks(overrides.schema);
564
+ const gaps = overrides.schema === undefined
565
+ ? []
566
+ : [...verticalGapWarnings(overrides.schema, 'PAGE-010 (uyarı)'), ...inertFlexPropWarning(inertFlexPropNodes(overrides.schema))];
567
+ const body = pageUpdateBody(current, overrides);
568
+ await platform.tenant('PUT', project, `/pages/${applicationId}/${pageId}`, { environment, body });
569
+ const summary = {
570
+ id: pageId,
571
+ changed: Object.keys(overrides),
572
+ preserved: { schemaNodes: body.schema.length, events: body.events.length }
573
+ };
574
+ if (gaps.length > 0)
575
+ summary.layoutWarnings = gaps;
576
+ return withNormalization(summary, [...reset, ...componentDefaultsNote(fixed), ...blockSizeNote(catalog.clamped), ...heightTypeNote(catalog.heights), ...reorderNote(moved)]);
577
+ }));
578
+ server.registerTool('page_clone', {
579
+ title: 'Sayfa klonla',
580
+ description: "Sayfanın kopyasını oluşturur (şema, olaylar ve flow'larıyla). Klon ana sayfa olarak işaretlenmez.",
581
+ inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
582
+ annotations: writes
583
+ }, handle(config, ({ project, environment, applicationId, pageId }) => platform.tenant('POST', project, `/pages/${applicationId}/clone/${pageId}`, { environment })));
584
+ server.registerTool('page_delete', {
585
+ title: 'Sayfa sil',
586
+ description: 'Sayfayı siler. Ana sayfa (isHomepage) silinemez; önce başka bir sayfayı ana sayfa yapın. Silmeden önce dependency_graph module:\'page\' ile bağımlılıkları kontrol edin.',
587
+ inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
588
+ annotations: destructive
589
+ }, handle(config, async ({ project, environment, applicationId, pageId }) => {
590
+ const page = unwrap(await platform.tenant('GET', project, `/pages/${applicationId}/${pageId}`, { environment }));
591
+ if (page.isHomepage === true) {
592
+ throw new PlatformApiError(`'${page.name}' ana sayfadır ve silinemez (CantDeleteHomepage); önce başka bir sayfa ana sayfa yapılmalı.`);
593
+ }
594
+ await platform.tenant('DELETE', project, `/pages/${applicationId}/${pageId}`, { environment });
595
+ return { deleted: pageId, name: page.name };
596
+ }));
597
+ }
@@ -0,0 +1,44 @@
1
+ import { listKnowledgeFiles, readKnowledgeFile } from '../resources/knowledge.js';
2
+ import { logCapabilityGap } from '../observability/trace.js';
3
+ import { handle } from '../platform/responses.js';
4
+ import { readOnly } from './common.js';
5
+ import { z } from 'zod';
6
+ export function registerPlatformTools(server, platform) {
7
+ const config = platform.config;
8
+ server.registerTool('platform_whoami', {
9
+ title: 'Oturum bilgisi',
10
+ description: 'Gateway\'in platformda kimin adına çalıştığını döner; auth kurulumunu doğrulamak için ilk çağrılacak tool.',
11
+ inputSchema: {},
12
+ annotations: readOnly
13
+ }, handle(config, () => platform.org('GET', '/connect/userinfo')));
14
+ server.registerTool('platform_guide', {
15
+ title: 'Platform rehberi oku',
16
+ description: "Modülün iş kuralları rehberini döner (workflow, microservice, entity, page, component, html, middleware, view, expressions, getting-started, modify-existing). Bir modülde İLK yazma işleminden önce ilgili rehberi okuyun; sayfa/layout/flow işleri için page, komponent modeli/olay sözleşmeleri için component, hazır komponentler yetmediğinde HTML kaçış kapısı (Mustache, fireEvent, script kuralları) için html, auth/oturum/yetki için middleware, tam metin arama indeksi için view, ifade yazarken kullanılabilir fonksiyon/bağlam sözlüğü (system.*, file.*, Page.*, state.*, Steps…) için expressions rehberi. Örnek gövdeler için ngapps://examples kaynaklarına bakın.",
17
+ inputSchema: {
18
+ guide: z.string().describe('Rehber adı; bilinmeyen ad verilirse mevcut rehberler listelenir')
19
+ },
20
+ annotations: readOnly
21
+ }, handle(config, async ({ guide }) => {
22
+ const available = listKnowledgeFiles('guides').map(file => file.replace(/\.md$/, ''));
23
+ if (!available.includes(guide)) {
24
+ return { error: `'${guide}' diye bir rehber yok.`, available };
25
+ }
26
+ return { guide, content: readKnowledgeFile('guides', `${guide}.md`), examples: listKnowledgeFiles('examples').map(file => `ngapps://examples/${file.replace(/\.json$/, '')}`) };
27
+ }));
28
+ server.registerTool('platform_gap_report', {
29
+ title: 'Kabiliyet boşluğu bildir',
30
+ description: "Yapmak İSTEDİĞİNİZ ama platform desteklemediği için yapamadığınız her noktada BUNU ÇAĞIRIN — tasarımı sessizce küçültmeyin. Bu çağrı platforma gitmez, yalnızca günlüğe 'KABİLİYET BOŞLUĞU' bloğu yazar; hata günlüğünde görünmeyen bu kayıtlar ürün yol haritasının girdisidir. Önce rehbere bakıp (platform_guide, özellikle hazır komponent yetmiyorsa 'html' kaçış kapısı) gerçekten desteklenmediğinden emin olun: desteklenen bir şeyi 'yok' diye bildirmek yanlış yol haritası üretir.",
31
+ inputSchema: {
32
+ goal: z.string().min(1).describe('Ne yapmak istediniz — kullanıcıya görünen sonuç olarak yazın ("randevu listesini hasta adına göre süzmek")'),
33
+ blocker: z.string().min(1).describe('Neden yapamadınız — hangi kabiliyetin eksik olduğu ("uçta X yok", "komponentte Y prop\'u yok")'),
34
+ severity: z.enum(['blocker', 'workaround', 'cosmetic']).describe('blocker: özellik hiç yapılamadı · workaround: daha kötü bir yolla yapıldı · cosmetic: yalnız görsel/ergonomik kayıp'),
35
+ module: z.string().optional().describe('İlgili modül: entity | microservice | workflow | page | component | middleware | view | flow'),
36
+ attempted: z.array(z.string()).optional().describe('Denenen tool/yaklaşımlar — boşluğun gerçekten araştırıldığını gösterir'),
37
+ workaround: z.string().optional().describe('Bunun yerine ne yaptınız; hiçbir şey yapılamadıysa boş bırakın')
38
+ },
39
+ annotations: readOnly
40
+ }, handle(config, async ({ goal, blocker, severity, module, attempted, workaround }) => {
41
+ logCapabilityGap({ goal, blocker, severity, module, attempted, workaround });
42
+ return { recorded: true, severity, module: module ?? null, note: 'Günlüğe yazıldı. Kullanıcıya da tek cümleyle söyleyin: neyi yapamadınız ve yerine ne yaptınız.' };
43
+ }));
44
+ }