@appaflytech/zeyrek-ai-mcp 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.env.example +8 -3
  2. package/.env.production.example +2 -0
  3. package/README.md +229 -193
  4. package/dist/config.js +20 -2
  5. package/dist/knowledge/backendConstraints.js +897 -0
  6. package/dist/knowledge/componentCatalog.js +196 -42
  7. package/dist/knowledge/coverage.js +43 -0
  8. package/dist/knowledge/flowGuards.js +30 -0
  9. package/dist/knowledge/guideSections.js +49 -0
  10. package/dist/knowledge/hydrate.js +97 -0
  11. package/dist/knowledge/languageCodes.js +36 -0
  12. package/dist/knowledge/languages.js +105 -0
  13. package/dist/knowledge/layoutSummary.js +10 -0
  14. package/dist/knowledge/localization.js +51 -0
  15. package/dist/knowledge/normalize.js +449 -16
  16. package/dist/knowledge/outlines.js +136 -12
  17. package/dist/knowledge/propTypes.js +80 -0
  18. package/dist/knowledge/rules.js +618 -43
  19. package/dist/knowledge/schemaGuard.js +199 -0
  20. package/dist/knowledge/styleResolve.js +63 -4
  21. package/dist/observability/trace.js +27 -3
  22. package/dist/platform/errors.js +46 -4
  23. package/dist/platform/http.js +33 -4
  24. package/dist/platform/patch.js +27 -0
  25. package/dist/platform/refs.js +104 -0
  26. package/dist/platform/responses.js +9 -2
  27. package/dist/platform/screenshot.js +527 -16
  28. package/dist/platform/secrets.js +40 -0
  29. package/dist/platform/tokenVault.js +89 -0
  30. package/dist/platform/writeDiff.js +281 -0
  31. package/dist/resources/examples/layout-app-shell.json +1 -1
  32. package/dist/resources/examples/microservice-table-list.json +2 -4
  33. package/dist/resources/examples/page-dynamic-form.json +2 -2
  34. package/dist/resources/examples/page-flex-sections.json +99 -0
  35. package/dist/resources/examples/page-sign-in.json +3 -2
  36. package/dist/resources/guides/component.md +69 -8
  37. package/dist/resources/guides/entity.md +404 -16
  38. package/dist/resources/guides/expressions.md +44 -2
  39. package/dist/resources/guides/external-component.md +9 -0
  40. package/dist/resources/guides/getting-started.md +77 -7
  41. package/dist/resources/guides/layout.md +53 -10
  42. package/dist/resources/guides/microservice.md +226 -13
  43. package/dist/resources/guides/middleware.md +89 -1
  44. package/dist/resources/guides/page.md +383 -46
  45. package/dist/resources/guides/translation.md +126 -8
  46. package/dist/resources/guides/view.md +16 -1
  47. package/dist/resources/guides/workflow.md +198 -27
  48. package/dist/resources/knowledge.js +5 -0
  49. package/dist/resources/rules.js +27 -0
  50. package/dist/resources/schema-fingerprint.json +8 -0
  51. package/dist/resources/schemas/CreateBulkRowCommand.json +33 -0
  52. package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
  53. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
  54. package/dist/resources/schemas/CreateEntityCommand.json +52 -12
  55. package/dist/resources/schemas/CreateMicroserviceCommand.json +26 -2
  56. package/dist/resources/schemas/CreateQueryCommand.json +56 -3
  57. package/dist/resources/schemas/CreateRowCommand.json +38 -0
  58. package/dist/resources/schemas/CreateViewCommand.json +13 -10
  59. package/dist/resources/schemas/CreateWorkflowCommand.json +95 -14
  60. package/dist/resources/schemas/GetRowsQuery.json +3 -0
  61. package/dist/resources/schemas/PreviewMicroserviceCommand.json +16 -0
  62. package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
  63. package/dist/resources/schemas/PreviewWorkflowCommand.json +34 -9
  64. package/dist/resources/schemas/StepDto.json +23 -3
  65. package/dist/resources/schemas/UpdateBulkRowCommand.json +33 -0
  66. package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
  67. package/dist/resources/schemas/UpdateEntityCommand.json +49 -10
  68. package/dist/resources/schemas/UpdateMicroserviceCommand.json +28 -3
  69. package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
  70. package/dist/resources/schemas/UpdateRowCommand.json +40 -0
  71. package/dist/resources/schemas/UpdateViewCommand.json +15 -10
  72. package/dist/resources/schemas/UpdateWorkflowCommand.json +95 -14
  73. package/dist/server.js +4 -0
  74. package/dist/tools/access.js +3 -3
  75. package/dist/tools/applications.js +1 -1
  76. package/dist/tools/automations.js +63 -30
  77. package/dist/tools/common.js +39 -0
  78. package/dist/tools/components.js +16 -7
  79. package/dist/tools/datasources.js +83 -21
  80. package/dist/tools/deployments.js +34 -2
  81. package/dist/tools/dtos.js +25 -12
  82. package/dist/tools/entities.js +113 -28
  83. package/dist/tools/externalComponents.js +9 -3
  84. package/dist/tools/files.js +20 -6
  85. package/dist/tools/flows.js +110 -11
  86. package/dist/tools/folders.js +7 -2
  87. package/dist/tools/globalFunctions.js +63 -0
  88. package/dist/tools/layouts.js +103 -29
  89. package/dist/tools/microservices.js +229 -38
  90. package/dist/tools/organizations.js +5 -6
  91. package/dist/tools/pages.js +365 -100
  92. package/dist/tools/platformInfo.js +152 -11
  93. package/dist/tools/projects.js +183 -33
  94. package/dist/tools/queries.js +32 -10
  95. package/dist/tools/rows.js +159 -45
  96. package/dist/tools/screenWrite.js +59 -0
  97. package/dist/tools/screens.js +28 -0
  98. package/dist/tools/translations.js +24 -8
  99. package/dist/tools/views.js +20 -10
  100. package/dist/tools/workflows.js +33 -16
  101. package/package.json +6 -3
@@ -1,7 +1,10 @@
1
- import { guid, projectParam, environmentParam, definitionParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
1
+ import { guid, entityRef, resolveEntityId, withResolved, projectParam, environmentParam, definitionParam, pagingParams, readOnly, writes, updates, destructive, microOps } from './common.js';
2
+ import { RefResolver, isGuid } from '../platform/refs.js';
2
3
  import { evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/policy.js';
3
4
  import { splitJunctionFields, withNormalization } from '../knowledge/normalize.js';
4
- import { entityOutline, resolvePart } from '../knowledge/outlines.js';
5
+ import { unmappedMembers, introducedUnmappedMembers, withUnmappedMembers } from '../knowledge/schemaGuard.js';
6
+ import { entityOutline, resolvePart, resolvePatchOperations, patchedSections } from '../knowledge/outlines.js';
7
+ import { patchResult, baseVersionOf } from '../platform/patch.js';
5
8
  import { isJunctionField, policyRules } from '../knowledge/rules.js';
6
9
  import { handle } from '../platform/responses.js';
7
10
  import fastJsonPatch from 'fast-json-patch';
@@ -20,6 +23,52 @@ function preserveApplicationBinding(definition) {
20
23
  if (applicationIds.length > 0)
21
24
  definition.applicationIds = applicationIds;
22
25
  }
26
+ /**
27
+ * `UpdateEntityCommandHandler` gövdedeki `Indexes` null ise onu BOŞ LİSTEYE çeviriyor
28
+ * (`request.Indexes?…ToList() ?? []`) ve `Entity.FindProcessedIndexes` boş listede entity'nin
29
+ * bütün index'lerini silinecek kümeye koyuyor: kayıt düşer, `DROP INDEX` koşar, hata dönmez.
30
+ * Domain katmanı null'ı özellikle koruyor, komut katmanı bu korumayı geçersiz kılıyor.
31
+ *
32
+ * Gateway alan verilmemişse mevcut index'leri gövdeye taşır. Bilinçli silme yolu kapanmaz:
33
+ * `indexes: []` AÇIKÇA yazılmışsa buraya hiç girilmez.
34
+ */
35
+ async function carryIndexes(platform, resolver, project, environment, datasourceId, id, body) {
36
+ const entity = datasourceId && isGuid(id) ? { entityId: id, datasourceId } : await resolver.entity(id);
37
+ const current = await platform.tenant('GET', project, `/entities/${entity.datasourceId}/${entity.entityId}`, { environment });
38
+ const definition = (current?.data ?? current);
39
+ const indexes = Array.isArray(definition?.indexes) ? definition.indexes : [];
40
+ if (indexes.length === 0)
41
+ return [];
42
+ body.indexes = indexes;
43
+ return [`Gövdede 'indexes' yoktu: platform bu durumda entity'nin BÜTÜN index'lerini siler, bu yüzden mevcut ${indexes.length} index gövdeye taşındı. Silmek istiyorsanız 'indexes: []' yazarak açıkça isteyin.`];
44
+ }
45
+ /**
46
+ * `Entity.FindProcessedIndexes` farkı YALNIZ `id` üzerinden alıyor: aynı `id` ile gelen index ne
47
+ * eklenenler ne silinenler kümesinde olduğu için `EntityService.UpdateAsync` ona hiç dokunmuyor.
48
+ * Ad, unique bayrağı ya da alan listesi değişse bile ne kayıt ne DDL güncellenir; istek 200 döner
49
+ * ve yama uygulanmış görünür. Politika kuralı olamaz — kural yalnız yamalanmış gövdeyi görür,
50
+ * bu karşılaştırma ÖNCEKİ hâli de ister.
51
+ */
52
+ function inPlaceIndexEdits(before, after) {
53
+ const asIndexes = (value) => {
54
+ const source = (value && typeof value === 'object' ? value : {});
55
+ return Array.isArray(source.indexes)
56
+ ? source.indexes.filter((index) => Boolean(index) && typeof index === 'object')
57
+ : [];
58
+ };
59
+ const shape = (index) => JSON.stringify([index.name ?? null, index.unique ?? false, Array.isArray(index.fieldIds) ? index.fieldIds : []]);
60
+ const original = new Map(asIndexes(before)
61
+ .filter(index => typeof index.id === 'string')
62
+ .map(index => [index.id, shape(index)]));
63
+ return asIndexes(after)
64
+ .filter(index => typeof index.id === 'string' && original.has(index.id) && original.get(index.id) !== shape(index))
65
+ .map(index => ({
66
+ ruleId: 'ENT-014',
67
+ severity: 'warn',
68
+ message: `'${index.id}' id'li index'in tanımı değişti ama platform var olan bir index'i YERİNDE GÜNCELLEMEZ: farkı yalnız 'id' üzerinden alır, aynı id'yi taşıyan kaydı ne siler ne yeniden kurar. İstek 200 dönse de veritabanında hiçbir şey değişmez. Index'i kaldırıp yeni bir id ile yeniden ekleyin.`,
69
+ guide: 'ngapps://guides/entity'
70
+ }));
71
+ }
23
72
  export function registerEntityTools(server, platform) {
24
73
  const config = platform.config;
25
74
  server.registerTool('entity_list', {
@@ -38,39 +87,42 @@ export function registerEntityTools(server, platform) {
38
87
  }, handle(config, ({ project, environment, datasourceId, ...query }) => platform.tenant('GET', project, `/entities/${datasourceId}`, { environment, query })));
39
88
  server.registerTool('entity_get', {
40
89
  title: 'Entity getir',
41
- description: "Entity'yi alan tanımlarıyla getirir. Çok alanlı entity'de önce view:'outline' (alan listesi), sonra view:'part' ile tek alan çekin.",
90
+ description: "Entity'yi alan tanımlarıyla getirir. Çok alanlı entity'de önce view:'outline' (alan listesi), sonra view:'part' ile tek alan çekin. Çok dilli entity'de kök alanlar companion bağını taşır: parent'ta `localizationEntityId`, companion'da `localizationParentId` — companion'ı ad kalıbından TAHMİN ETMEYİN, bu bağdan bulun.",
42
91
  inputSchema: {
43
92
  project: projectParam,
44
93
  environment: environmentParam,
45
- datasourceId: guid,
46
- id: guid,
94
+ datasourceId: guid.optional().describe('Verilmezse gateway datasource ağacından bulur'),
95
+ id: entityRef,
47
96
  view: z.enum(['full', 'outline', 'part']).optional().describe('varsayılan full'),
48
97
  part: z.string().optional().describe("view:'part' için yol, ör. fields[slug=email] veya indexes[0]")
49
98
  },
50
99
  annotations: readOnly
51
100
  }, handle(config, async ({ project, environment, datasourceId, id, view, part }) => {
52
- const payload = await platform.tenant('GET', project, `/entities/${datasourceId}/${id}`, { environment });
101
+ const resolver = new RefResolver(platform, project, environment);
102
+ const entity = datasourceId && isGuid(id) ? { entityId: id, datasourceId } : await resolver.entity(id);
103
+ const payload = await platform.tenant('GET', project, `/entities/${entity.datasourceId}/${entity.entityId}`, { environment });
53
104
  const definition = (payload?.data ?? payload);
54
105
  if (view === 'outline')
55
- return entityOutline(definition);
106
+ return withResolved(entityOutline(definition), resolver.resolved);
56
107
  if (view === 'part') {
57
108
  if (!part)
58
109
  throw new Error("view:'part' için 'part' parametresi zorunlu.");
59
- return { part, value: resolvePart(definition, part) };
110
+ return withResolved({ part, value: resolvePart(definition, part) }, resolver.resolved);
60
111
  }
61
- return payload;
112
+ return withResolved(payload, resolver.resolved);
62
113
  }));
63
114
  server.registerTool('entity_create', {
64
115
  title: 'Entity oluştur',
65
- description: "Datasource altında yeni entity oluşturur (definition = CreateEntityCommand: name, fields[], applicationIds, ...). İlişkiler ve alan tipi kararı için platform_guide('entity') okuyun. Gövde politika kontrolünden geçer. applicationIds create'te platformca bağlanır (CreateEntityCommandHandler entityApplications'ı Entity ctor'una geçirir); satır uçları bu bağa İHTİYAÇ DUYMAZ — bağ yalnız Studio'nun application'a göre entity listesini ve bağımlılık sorgularını etkiler.",
116
+ description: "Datasource altında yeni entity oluşturur (definition = CreateEntityCommand: name, fields[], applicationIds, ...). İlişkiler ve alan tipi kararı için platform_guide('entity') okuyun. Gövde politika kontrolünden geçer. applicationIds create'te platformca bağlanır (CreateEntityCommandHandler entityApplications'ı Entity ctor'una geçirir); satır uçları bu bağa İHTİYAÇ DUYMAZ — bağ yalnız Studio'nun application'a göre entity listesini ve bağımlılık sorgularını etkiler. `fields[].isLocalized: true` alanı ana tablodan çıkarıp companion `<slug>_localization` entity'sine taşır (yalnız düz METİN alanı; anahtar/unique/ilişki olamaz) — platform_guide('entity') §'Çoklu dilli alan'.",
66
117
  inputSchema: { project: projectParam, environment: environmentParam, datasourceId: guid, definition: definitionParam, policyOverride: policyOverrideParam },
67
118
  annotations: writes
68
119
  }, handle(config, async ({ project, environment, datasourceId, definition, policyOverride }) => {
69
120
  const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'entity', definition, { operation: 'create' }, policyOverride));
70
121
  const { base, deferred } = splitJunctionFields(definition, isJunctionField);
122
+ const drift = unmappedMembers('CreateEntityCommand', base);
71
123
  const payload = await platform.tenant('POST', project, `/entities/${datasourceId}`, { environment, body: base });
72
124
  if (deferred.length === 0)
73
- return attachFindings(payload, findings);
125
+ return attachFindings(withUnmappedMembers(payload, drift), findings);
74
126
  const created = (payload?.data ?? payload);
75
127
  const createdId = String(created?.id ?? '');
76
128
  if (!createdId) {
@@ -82,7 +134,7 @@ export function registerEntityTools(server, platform) {
82
134
  preserveApplicationBinding(full);
83
135
  const updated = await platform.tenant('PUT', project, `/entities/${createdId}`, { environment, body: full });
84
136
  const note = `m2m/çoklu alanlar (${deferred.map(field => field.slug ?? field.name).join(', ')}) ayrı bir güncelleme turunda eklendi: create yolu junction kaydını açar ama fiziksel tabloyu kurmaz (platform bug'ı P-01), update yolu kurar.`;
85
- return attachFindings(withNormalization(updated, [note]), findings);
137
+ return attachFindings(withUnmappedMembers(withNormalization(updated, [note]), drift), findings);
86
138
  }));
87
139
  server.registerTool('entity_create_discovered', {
88
140
  title: 'Keşfedilen entity\'leri içe al',
@@ -92,34 +144,67 @@ export function registerEntityTools(server, platform) {
92
144
  }, handle(config, ({ project, environment, datasourceId, definition }) => platform.tenant('POST', project, `/entities/${datasourceId}/discovered`, { environment, body: definition })));
93
145
  server.registerTool('entity_update', {
94
146
  title: 'Entity güncelle',
95
- description: 'Entity tanımını günceller (tam gövde). Hedefli değişiklik için entity_patch tercih edin. Gövde politika kontrolünden geçer.',
96
- inputSchema: { project: projectParam, environment: environmentParam, id: guid, definition: definitionParam, policyOverride: policyOverrideParam },
147
+ description: "Entity tanımını günceller (tam gövde). Hedefli değişiklik için entity_patch tercih edin. Gövde politika kontrolünden geçer. DİKKAT: gövdede 'indexes' YOKSA platform entity'nin bütün index'lerini siler — gateway alan verilmemişse mevcut index'leri taşır ve bunu bildirir; silmek için 'indexes: []' yazın. `fields[].isLocalized: true` alanı ana tablodan çıkarıp companion `<slug>_localization` entity'sine taşır (yalnız düz METİN alanı; anahtar/unique/ilişki olamaz) — platform_guide('entity') §'Çoklu dilli alan'.",
148
+ inputSchema: { project: projectParam, environment: environmentParam, datasourceId: guid.optional().describe('Verilmezse gateway datasource ağacından bulur'), id: entityRef, definition: definitionParam, policyOverride: policyOverrideParam },
97
149
  annotations: updates
98
- }, handle(config, async ({ project, environment, id, definition, policyOverride }) => {
150
+ }, handle(config, async ({ project, environment, datasourceId, id, definition, policyOverride }) => {
99
151
  const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'entity', definition, { operation: 'update' }, policyOverride));
100
- const payload = await platform.tenant('PUT', project, `/entities/${id}`, { environment, body: definition });
101
- return attachFindings(payload, findings);
152
+ const resolver = new RefResolver(platform, project, environment);
153
+ const body = { ...definition };
154
+ const notes = Object.hasOwn(body, 'indexes') ? [] : await carryIndexes(platform, resolver, project, environment, datasourceId, id, body);
155
+ const entityId = await resolveEntityId(resolver, id);
156
+ const drift = unmappedMembers('UpdateEntityCommand', definition);
157
+ const payload = await platform.tenant('PUT', project, `/entities/${entityId}`, { environment, body });
158
+ return attachFindings(withUnmappedMembers(withResolved(withNormalization(payload, notes), resolver.resolved), drift), findings);
102
159
  }));
103
160
  server.registerTool('entity_patch', {
104
161
  title: 'Entity\'ye patch uygula',
105
- description: "Tam gövdeyi bağlama almadan hedefli değişiklik: gateway mevcut tanımı çeker, JSON Patch uygular, politika kontrolünden geçirir, geri yazar. Yanıt outline özetidir. GET 'applications' döner ama UpdateEntityCommand 'applicationIds' okur; gateway bu çeviriyi kendisi yapar, yoksa her patch application bağını sessizce siler.",
106
- inputSchema: { project: projectParam, environment: environmentParam, datasourceId: guid, id: guid, operations: patchOperationsParam, policyOverride: policyOverrideParam },
162
+ description: "Tam gövdeyi bağlama almadan hedefli değişiklik: gateway mevcut tanımı okur, yolları çözer ve op'ları platformun PATCH /entities/{id} ucuna `{ baseVersion, ops }` olarak geçirir — tam gövde PUT'u ATILMAZ, yalnız fark yazılır. Yanıt outline özetidir. YOLLARDA SEÇİCİ KULLANIN, indeks değil: '/fields[slug=email]/nullable' — indeksle yazmak alan sırası değişince SESSİZCE yanlış alanı yazar. Gateway seçiciyi indekse çevirir ve hangi kayda indiğini yanıttaki patchTargets ile söyler. `baseVersion` okunan kaydın `version` alanından gelir; kayıt bu arada değiştiyse platform 409 döner ve HİÇBİR op uygulanmaz. 'applications' → 'applicationIds' çevirisini artık platform yapar. `fields[].isLocalized: true` alanı ana tablodan çıkarıp companion `<slug>_localization` entity'sine taşır (yalnız düz METİN alanı; anahtar/unique/ilişki olamaz) — platform_guide('entity') §'Çoklu dilli alan'.",
163
+ inputSchema: { project: projectParam, environment: environmentParam, datasourceId: guid.optional().describe('Verilmezse gateway datasource ağacından bulur'), id: entityRef, operations: patchOperationsParam.optional(), ops: patchOperationsParam.optional().describe("'operations' ile aynı alan (eşanlamlı) — sayfa micro-op tool'larıyla aynı adı kullanabilmeniz için"), policyOverride: policyOverrideParam },
107
164
  annotations: updates
108
- }, handle(config, async ({ project, environment, datasourceId, id, operations, policyOverride }) => {
109
- const current = await platform.tenant('GET', project, `/entities/${datasourceId}/${id}`, { environment });
165
+ }, handle(config, async ({ project, environment, datasourceId, id, operations: givenOperations, ops, policyOverride }) => {
166
+ const operations = microOps('entity_patch', ops, givenOperations);
167
+ const resolver = new RefResolver(platform, project, environment);
168
+ const entity = datasourceId && isGuid(id) ? { entityId: id, datasourceId } : await resolver.entity(id);
169
+ const current = await platform.tenant('GET', project, `/entities/${entity.datasourceId}/${entity.entityId}`, { environment });
110
170
  const definition = structuredClone((current?.data ?? current));
111
- const patched = fastJsonPatch.applyPatch(definition, operations, true, false).newDocument;
112
- preserveApplicationBinding(patched);
113
- const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'entity', patched, { operation: 'update' }, policyOverride));
114
- await platform.tenant('PUT', project, `/entities/${id}`, { environment, body: patched });
115
- return attachFindings({ applied: operations.length, outline: entityOutline(patched) }, findings);
171
+ const resolvedOps = resolvePatchOperations(definition, operations, 'entity_get');
172
+ // Politika, yazmadan önce sonucu görmeli; yerel kopya YALNIZ değerlendirme için, gövde olarak gönderilmez.
173
+ const preview = fastJsonPatch.applyPatch(definition, resolvedOps.operations, true, false).newDocument;
174
+ const findings = [
175
+ ...enforcePolicy(evaluatePolicy(config, policyRules, 'entity', preview, { operation: 'update' }, policyOverride)),
176
+ ...inPlaceIndexEdits(current?.data ?? current, preview)
177
+ ];
178
+ // Yalnız YAMANIN eklediği anahtar bildirilir: gövdenin geri kalanını gateway GET'ten taşıdı
179
+ // ve orada komutta olmayan yedi yapısal alan var (ölçüldü — g-03 Adım 0). Onları bildirmek
180
+ // kanalı gürültüye boğar ve gerçek sinyali gömer.
181
+ const drift = introducedUnmappedMembers('UpdateEntityCommand', (current?.data ?? current), preview);
182
+ const saved = await platform.tenant('PATCH', project, `/entities/${entity.entityId}`, {
183
+ environment,
184
+ body: { baseVersion: baseVersionOf(definition, 'entity_get'), ops: resolvedOps.operations }
185
+ });
186
+ const written = patchResult(saved, preview);
187
+ const touched = patchedSections(written.data, resolvedOps.operations);
188
+ return attachFindings(withUnmappedMembers(withResolved({
189
+ applied: operations.length,
190
+ newVersion: written.newVersion,
191
+ patchTargets: resolvedOps.targets,
192
+ ...(resolvedOps.notes.length > 0 ? { resolvedPaths: resolvedOps.notes } : {}),
193
+ // S-8: yazılanın kanıtı yanıtta dursun; dokunulmayan bölümler outline'da kalır.
194
+ ...(Object.keys(touched).length > 0 ? { touched } : {}),
195
+ outline: entityOutline(written.data)
196
+ }, resolver.resolved), drift), findings);
116
197
  }));
117
198
  server.registerTool('entity_delete', {
118
199
  title: 'Entity sil',
119
200
  description: 'Entity tanımını siler; önce entity_dependencies ile bağımlılıklarını kontrol edin.',
120
- inputSchema: { project: projectParam, environment: environmentParam, id: guid },
201
+ inputSchema: { project: projectParam, environment: environmentParam, id: entityRef },
121
202
  annotations: destructive
122
- }, handle(config, ({ project, environment, id }) => platform.tenant('DELETE', project, `/entities/${id}`, { environment })));
203
+ }, handle(config, async ({ project, environment, id }) => {
204
+ const resolver = new RefResolver(platform, project, environment);
205
+ const entityId = await resolveEntityId(resolver, id);
206
+ return withResolved(await platform.tenant('DELETE', project, `/entities/${entityId}`, { environment }), resolver.resolved);
207
+ }));
123
208
  server.registerTool('entity_clone', {
124
209
  title: 'Entity klonla',
125
210
  description: 'Entity\'nin kopyasını oluşturur.',
@@ -4,8 +4,11 @@ import { PlatformApiError } from '../platform/errors.js';
4
4
  import { handle } from '../platform/responses.js';
5
5
  import { basename, join, resolve } from 'node:path';
6
6
  import { z } from 'zod';
7
- /** Platformun köprü protokolü; scaffold ve publish bu sürüme göre üretir/doğrular */
8
- const PROTOCOL_VERSION = 1;
7
+ import { evaluatePolicy, enforcePolicy } from '../knowledge/policy.js';
8
+ import { policyRules } from '../knowledge/rules.js';
9
+ import { EXTERNAL_COMPONENT_PROTOCOL_VERSION } from '../knowledge/backendConstraints.js';
10
+ /** Platformun köprü protokolü; scaffold ve publish bu sürüme göre üretir/doğrular (backend ExternalComponentRules.CurrentProtocolVersion) */
11
+ const PROTOCOL_VERSION = EXTERNAL_COMPONENT_PROTOCOL_VERSION;
9
12
  /** Backend `ExternalComponentRules.MaxBundleSizeBytes` ile aynı sınır */
10
13
  const maxBundleBytes = 2 * 1024 * 1024;
11
14
  const slugPattern = /^x-[a-z][a-z0-9-]*$/;
@@ -185,7 +188,7 @@ export function registerExternalComponentTools(server, platform) {
185
188
  }));
186
189
  server.registerTool('external_component_publish', {
187
190
  title: 'Harici komponenti yayınla',
188
- description: "Komponent kaydını ve paketini platforma yükler. Paket sunucudaki doğrulama hattından geçer (şema + güvenlik taraması); block bulgusu varsa depolamaya hiç yazılmaz ve rapor döner. Kabul edilen paket taslak (draft) olarak kaydedilir — Studio önizlemesinde görünür, yayınlanmış uygulamada görünmez; yayına almak için external_component_activate. Dosyalar gateway sürecinin okuyabildiği yerel yollardır, içerikleri model bağlamına girmez.",
191
+ description: "Komponent kaydını ve paketini platforma yükler. Paket sunucudaki doğrulama hattından geçer (şema + güvenlik taraması); block bulgusu varsa depolamaya hiç yazılmaz ve rapor döner. Kabul edilen paket taslak (draft) olarak kaydedilir — Studio önizlemesinde görünür, yayınlanmış uygulamada görünmez; yayına almak için external_component_activate. Dosyalar gateway sürecinin okuyabildiği yerel yollardır, içerikleri model bağlamına girmez. Backend ön kontrolü (EXT-001, block): protocolVersion 1, definition.blocks dolu, props[].name ayrılmış DOM adı ya da zx/on önekli olamaz, props[].type sözlükten (ngapps://rules/backend-constraints).",
189
192
  inputSchema: {
190
193
  project: projectParam,
191
194
  environment: environmentParam,
@@ -219,6 +222,9 @@ export function registerExternalComponentTools(server, platform) {
219
222
  if (bytes.byteLength > maxBundleBytes) {
220
223
  throw new PlatformApiError(`external_component_publish: paket ${maxBundleBytes} bayt sınırını aşıyor (${bytes.byteLength} B).`);
221
224
  }
225
+ // EXT-001: protocolVersion, definition.blocks, ayrılmış prop adları ve prop tipleri — kayıt
226
+ // ucu 422 dönmeden manifest'te yakalanır; paket yükleme ikinci istek olduğu için yarım kayıt kalmaz.
227
+ enforcePolicy(evaluatePolicy(config, policyRules, 'externalComponent', { ...manifest, slug }, { operation: 'create' }));
222
228
  const record = await platform.tenant('POST', project, `/components/${applicationId}/external`, {
223
229
  environment,
224
230
  body: {
@@ -4,9 +4,12 @@ import { handle } from '../platform/responses.js';
4
4
  import { readFile, writeFile } from 'node:fs/promises';
5
5
  import { basename, extname, resolve } from 'node:path';
6
6
  import { z } from 'zod';
7
+ import { FORBIDDEN_NAME_CHARACTERS } from '../knowledge/backendConstraints.js';
7
8
  const maxInlineBytes = 8 * 1024 * 1024;
8
9
  const maxUploadBytes = 50 * 1024 * 1024;
9
- const forbiddenNameChars = `\\|!#~$%&/=?»«@£§€{};'<>,`;
10
+ /** Metin içerik bağlamdan geçtiği için base64'ün 8 MB sınırından çok daha dar tutulur (G-26). */
11
+ const maxTextBytes = 1024 * 1024;
12
+ const forbiddenNameChars = FORBIDDEN_NAME_CHARACTERS;
10
13
  const mimeTypes = {
11
14
  '.css': 'text/css',
12
15
  '.csv': 'text/csv',
@@ -51,20 +54,22 @@ export function registerFileTools(server, platform) {
51
54
  }, handle(config, ({ project, environment, id }) => platform.tenant('GET', project, `/files/${id}`, { environment })));
52
55
  server.registerTool('file_upload', {
53
56
  title: 'Dosya yükle',
54
- description: "Dosyayı projenin depolamasına yükler ve FileDto (id + serve adresleri) döner. İçerik iki yoldan verilir: `path` — gateway sürecinin okuyabildiği yerel yol (büyük dosyalar için tercih edilir, içerik model bağlamına girmez), ya da `content` — base64 (8 MB'a kadar) + `filename`. Platform sınırı 50 MB; dosya adı \\|!#~$%&/=?»«@£§€{};'<>, karakterlerini içeremez. Yüklenen dosya sayfada `image`/`file-viewer` komponentiyle gösterilir ya da satır alanına yol olarak yazılır.",
57
+ description: "Dosyayı projenin depolamasına yükler ve FileDto (id + serve adresleri) döner. İçerik ÜÇ yoldan verilir: `text` — düz metin içerik (SVG, CSV, Markdown, JSON…) + `filename`; base64'e çevirmeye gerek yok, bunu gateway yapar. `path` — gateway sürecinin okuyabildiği yerel yol (büyük ve ikili dosyalar için). `content` — base64 (8 MB'a kadar) + `filename`. Platform sınırı 50 MB; dosya adı \\|!#~$%&/=?»«@£§€{};'<>, karakterlerini içeremez. Yüklenen dosya sayfada `image`/`file-viewer` komponentiyle gösterilir ya da satır alanına yol olarak yazılır.",
55
58
  inputSchema: {
56
59
  project: projectParam,
57
60
  environment: environmentParam,
58
61
  path: z.string().min(1).optional().describe("Gateway sürecinin okuyabildiği yerel dosya yolu; `content` ile birlikte verilmez"),
59
62
  content: z.string().min(1).optional().describe('Base64 kodlanmış dosya içeriği; `filename` ile birlikte zorunludur'),
63
+ text: z.string().min(1).optional().describe("Düz metin içerik (UTF-8): SVG, CSV, Markdown, JSON, düz metin. `filename` ile birlikte zorunludur ve base64'e çevirmeye gerek yoktur — yalnız MCP araçlarıyla çalışan bir ajanın elinde base64 kodlayıcı yok, bu alan o boşluğu kapatır (G-26). İkili dosyalar için `path` ya da `content` kullanın."),
60
64
  filename: z.string().min(1).optional().describe("`content` ile zorunlu; `path` verildiğinde dosya adını ezer"),
61
65
  contentType: z.string().min(1).optional().describe('Belirtilmezse uzantıdan çözülür, bilinmeyen uzantıda application/octet-stream'),
62
66
  folderId: guid.optional().describe('Hedef klasör (folder_list); boşsa kök')
63
67
  },
64
68
  annotations: writes
65
- }, handle(config, async ({ project, environment, path, content, filename, contentType, folderId }) => {
66
- if (!path === !content) {
67
- throw new PlatformApiError("file_upload: `path` ya da `content` parametrelerinden tam olarak biri verilmeli — `path` gateway'in okuyabildiği yerel dosya, `content` base64 içerik (`filename` ile).");
69
+ }, handle(config, async ({ project, environment, path, content, text, filename, contentType, folderId }) => {
70
+ const given = [path, content, text].filter(value => value !== undefined);
71
+ if (given.length !== 1) {
72
+ throw new PlatformApiError("file_upload: `path`, `content` ya da `text` parametrelerinden tam olarak biri verilmeli — `path` gateway'in okuyabildiği yerel dosya, `content` base64 içerik, `text` düz metin (ikisi de `filename` ile).");
68
73
  }
69
74
  let bytes;
70
75
  let name;
@@ -78,6 +83,15 @@ export function registerFileTools(server, platform) {
78
83
  }
79
84
  name = filename ?? basename(absolute);
80
85
  }
86
+ else if (text !== undefined) {
87
+ if (!filename)
88
+ throw new PlatformApiError('file_upload: `text` verildiğinde `filename` zorunludur.');
89
+ bytes = Buffer.from(text, 'utf8');
90
+ if (bytes.byteLength > maxTextBytes) {
91
+ throw new PlatformApiError(`file_upload: \`text\` içeriği ${maxTextBytes} bayt sınırını aşıyor (${bytes.byteLength} B). Büyük dosyalar için \`path\` ya da \`content\` kullanın.`);
92
+ }
93
+ name = filename;
94
+ }
81
95
  else {
82
96
  if (!filename)
83
97
  throw new PlatformApiError('file_upload: `content` verildiğinde `filename` zorunludur.');
@@ -94,7 +108,7 @@ export function registerFileTools(server, platform) {
94
108
  }
95
109
  const offendingChar = [...forbiddenNameChars].find(character => name.includes(character));
96
110
  if (offendingChar) {
97
- throw new PlatformApiError(`file_upload: dosya adı '${offendingChar}' karakterini içeriyor; platform doğrulaması bunu reddeder. Yasak karakterler: ${forbiddenNameChars}`);
111
+ throw new PlatformApiError(`file_upload: dosya adı '${offendingChar}' karakterini içeriyor; platform doğrulaması bunu 422 ile reddeder (FILE-001, UploadFileCommandValidator). Yasak karakterler: ${forbiddenNameChars}`);
98
112
  }
99
113
  const resolvedType = contentType ?? mimeTypes[extname(name).toLowerCase()] ?? 'application/octet-stream';
100
114
  const multipart = folderId
@@ -1,15 +1,36 @@
1
1
  import { fetchHost, flowPath, hostParam, hostUpdateBody, unwrap } from './screens.js';
2
- import { guid, projectParam, environmentParam, readOnly, writes, updates, destructive } from './common.js';
2
+ import { guid, projectParam, environmentParam, readOnly, writes, updates, destructive, microOps } from './common.js';
3
3
  import { evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/policy.js';
4
- import { normalizeFlowSteps, withNormalization } from '../knowledge/normalize.js';
4
+ import { normalizeFlowSteps, unknownSetterTargetWarnings, withNormalization } from '../knowledge/normalize.js';
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';
9
+ import { resolvePatchOperations } from '../knowledge/outlines.js';
10
+ import { patchResult, baseVersionOf } from '../platform/patch.js';
11
+ import fastJsonPatch from 'fast-json-patch';
8
12
  import { policyRules } from '../knowledge/rules.js';
9
13
  import { randomUUID } from 'node:crypto';
10
14
  import { z } from 'zod';
15
+ /**
16
+ * Akış yazması yanıtında gövdeyi geri döndürmüyor, bu yüzden diff için akışı bir kez okuruz.
17
+ * ÖLÇÜLDÜ 2026-09-05 (Corecruiter koşusu, `S-19`): `code` adımının gövdesi kaydedilmedi,
18
+ * `flow_create` 200 döndü, `flow_get` adımı gösterdi ama `args` içinde kodu taşıyan alan yoktu —
19
+ * teşhis ~10 dakika sürdü ve tek belirti filtrenin hiçbir şey yapmamasıydı. Okuma başarısız
20
+ * olursa yazma yine başarılıdır: diff bir teşhis eklentisidir, yazmanın sonucu değil.
21
+ */
22
+ async function flowWriteDrops(platform, host, project, environment, applicationId, hostId, flowId, sent) {
23
+ try {
24
+ const saved = unwrap(await platform.tenant('GET', project, flowPath(host, applicationId, hostId, flowId), { environment }));
25
+ return writeDiff(sent, saved);
26
+ }
27
+ catch {
28
+ return { dropped: [], normalized: [] };
29
+ }
30
+ }
11
31
  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 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.");
12
32
  const policyOverrideParam = z.boolean().optional();
33
+ const patchOperationsParam = z.array(z.record(z.unknown())).min(1).describe("RFC 6902 JSON Patch işlemleri; yolları flow_get çıktısıyla keşfedin. Dizi öğelerinde indeks yerine seçici kullanılabilir: '/steps[stepId=paint]/args/iteratees[element=tbl_recruiters]/mapping/source' — seçici gövde platforma gitmeden indekse çevrilir, çözülemezse istek hiç gönderilmez.");
13
34
  const flowIdParam = guid.describe("Akış id'si; şemadaki events[].value bu id'dir");
14
35
  function flowSummary(flow) {
15
36
  return {
@@ -22,7 +43,15 @@ function flowSummary(flow) {
22
43
  }
23
44
  async function hostEnvelope(platform, host, project, environment, applicationId, hostId) {
24
45
  const record = await fetchHost(platform, host, project, environment, applicationId, hostId);
25
- return { [host]: hostUpdateBody(host, { ...record, applicationId }) };
46
+ return { record, envelope: { [host]: hostUpdateBody(host, { ...record, applicationId }) } };
47
+ }
48
+ /**
49
+ * FLOW-024'ün arayacağı düğüm kümesi. Ham kaydın TAMAMI gezilmez: kayıt kendi `flows`'unu da
50
+ * taşıyabilir ve oradaki bayat element id'leri kümeyi kirletip aramayı sessizce boşa çıkarır.
51
+ * page/component düğümleri `schema`'da, layout'unkiler `views`'ta durur.
52
+ */
53
+ function hostNodes(record) {
54
+ return record.schema ?? record.views ?? [];
26
55
  }
27
56
  async function requiredParameterWarnings(platform, project, environment, applicationId, steps) {
28
57
  const warnings = [];
@@ -146,7 +175,7 @@ export function registerFlowTools(server, platform) {
146
175
  },
147
176
  annotations: writes
148
177
  }, handle(config, async ({ project, environment, host, applicationId, hostId, name, slug, steps, errorSteps, flowId, policyOverride }) => {
149
- const envelope = await hostEnvelope(platform, host, project, environment, applicationId, hostId);
178
+ const { record: hostRecord, envelope } = await hostEnvelope(platform, host, project, environment, applicationId, hostId);
150
179
  const resolvedFlowId = flowId ?? randomUUID();
151
180
  const flow = {
152
181
  id: resolvedFlowId,
@@ -160,16 +189,20 @@ export function registerFlowTools(server, platform) {
160
189
  const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'flow', flow, { operation: 'create' }, policyOverride));
161
190
  const parameterWarnings = [
162
191
  ...await requiredParameterWarnings(platform, project, environment, applicationId, steps),
163
- ...flowBodyWarnings(steps)
192
+ ...flowBodyWarnings(steps),
193
+ ...unknownSetterTargetWarnings(steps, errorSteps ?? [], hostNodes(hostRecord))
164
194
  ];
165
195
  await platform.tenant('POST', project, flowPath(host, applicationId, hostId), { environment, body: { flow, ...envelope } });
166
- return attachFindings(withNormalization({
196
+ // Diff YALNIZ çağıranın yazdığı alanları kapsar; `id` ve barındırıcı bağı (`pageId` vb.)
197
+ // gövdeye gateway koyar, kullanıcının düzeltebileceği bir şey değildir.
198
+ const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, resolvedFlowId, { name: flow.name, slug: flow.slug, steps: flow.steps, errorSteps: flow.errorSteps });
199
+ return attachFindings(withWriteDiff(withNormalization({
167
200
  host,
168
201
  hostId,
169
202
  flow: { id: resolvedFlowId, name, slug, steps: steps.length },
170
203
  ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
171
204
  hint: "Akışı bir olaya bağlamak için: page_schema_patch ops:[{op:'set_event', target:<elementId>, event:'<component_get events listesindeki ad>', eventValue:<flow id>}]. 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."
172
- }, normalized), findings);
205
+ }, normalized), drops), findings);
173
206
  }));
174
207
  server.registerTool('flow_update', {
175
208
  title: 'Akış güncelle',
@@ -192,7 +225,7 @@ export function registerFlowTools(server, platform) {
192
225
  const current = unwrap(await platform.tenant('GET', project, flowPath(host, applicationId, hostId, flowId), { environment }));
193
226
  if (!current.id)
194
227
  throw new PlatformApiError(`'${flowId}' id'li akış bulunamadı; flow_list ile doğrulayın.`);
195
- const envelope = await hostEnvelope(platform, host, project, environment, applicationId, hostId);
228
+ const { record: hostRecord, envelope } = await hostEnvelope(platform, host, project, environment, applicationId, hostId);
196
229
  const flow = {
197
230
  id: flowId,
198
231
  name: name ?? current.name,
@@ -208,10 +241,76 @@ export function registerFlowTools(server, platform) {
208
241
  : [];
209
242
  const parameterWarnings = [
210
243
  ...await requiredParameterWarnings(platform, project, environment, applicationId, flow.steps),
211
- ...flowBodyWarnings(flow.steps)
244
+ ...flowBodyWarnings(flow.steps),
245
+ ...unknownSetterTargetWarnings(flow.steps, flow.errorSteps, hostNodes(hostRecord))
212
246
  ];
213
247
  await platform.tenant('PUT', project, flowPath(host, applicationId, hostId, flowId), { environment, body: { flow, ...envelope } });
214
- return attachFindings(withNormalization({ host, hostId, flow: flowSummary(flow), ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}) }, normalized), findings);
248
+ // Kısmi güncellemede yalnız GÖNDERİLEN alanlar karşılaştırılır; verilmeyen alan için
249
+ // "düştü" demek yanlış alarm olurdu.
250
+ const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, flowId, {
251
+ ...(name === undefined ? {} : { name }),
252
+ ...(slug === undefined ? {} : { slug }),
253
+ ...(steps === undefined ? {} : { steps }),
254
+ ...(errorSteps === undefined ? {} : { errorSteps })
255
+ });
256
+ return attachFindings(withWriteDiff(withNormalization({ host, hostId, flow: flowSummary(flow), ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}) }, normalized), drops), findings);
257
+ }));
258
+ server.registerTool('flow_patch', {
259
+ title: "Akışa patch uygula",
260
+ description: "Tam gövdeyi bağlama almadan hedefli değişiklik: gateway akışı çeker, JSON Patch uygular, politika kontrolünden geçirip geri yazar. flow_update REPLACE olduğu için tek bir ifadeyi düzeltmek 10 adımlı gövdenin tamamını yeniden göndermek demekti (09-05 koşusunda aynı 11 KB'lik gövde beş kez gönderildi). Dizi öğelerinde indeks yerine SEÇİCİ kullanın — adım ve iteratee sırası platformda değişebilir: '/steps[stepId=paint]/args/iteratees[element=tbl_funnel]/mapping/mappings/share/ref' ya da '/steps[stepId=load_summary]/args/parameters[name=company]/value/ref'. Seçici çözülemezse istek HİÇ gönderilmez.",
261
+ inputSchema: {
262
+ project: projectParam,
263
+ environment: environmentParam,
264
+ host: hostParam,
265
+ applicationId: guid,
266
+ hostId: guid,
267
+ flowId: flowIdParam,
268
+ operations: patchOperationsParam.optional(),
269
+ ops: patchOperationsParam.optional().describe("'operations' ile aynı alan (eşanlamlı) — sayfa micro-op tool'larıyla aynı adı kullanabilmeniz için"),
270
+ policyOverride: policyOverrideParam
271
+ },
272
+ annotations: updates
273
+ }, handle(config, async ({ project, environment, host, applicationId, hostId, flowId, operations: givenOperations, ops, policyOverride }) => {
274
+ const operations = microOps('flow_patch', ops, givenOperations);
275
+ const current = unwrap(await platform.tenant('GET', project, flowPath(host, applicationId, hostId, flowId), { environment }));
276
+ if (!current.id)
277
+ throw new PlatformApiError(`'${flowId}' id'li akış bulunamadı; flow_list ile doğrulayın.`);
278
+ const document = structuredClone(current);
279
+ const resolved = resolvePatchOperations(document, operations, 'flow_get');
280
+ const patched = fastJsonPatch.applyPatch(document, resolved.operations, true, false).newDocument;
281
+ const { record: hostRecord } = await hostEnvelope(platform, host, project, environment, applicationId, hostId);
282
+ const flow = {
283
+ ...patched,
284
+ id: flowId,
285
+ steps: patched.steps ?? [],
286
+ errorSteps: patched.errorSteps ?? [],
287
+ ...(host === 'page' ? { pageId: hostId } : host === 'layout' ? { layoutId: hostId } : { componentId: hostId })
288
+ };
289
+ const normalized = [...normalizeFlowSteps(flow.steps), ...normalizeFlowSteps(flow.errorSteps)];
290
+ const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'flow', flow, { operation: 'update' }, policyOverride));
291
+ const parameterWarnings = [
292
+ ...await requiredParameterWarnings(platform, project, environment, applicationId, flow.steps),
293
+ ...flowBodyWarnings(flow.steps),
294
+ ...unknownSetterTargetWarnings(flow.steps, flow.errorSteps, hostNodes(hostRecord))
295
+ ];
296
+ // Gövde değil FARK gönderilir: barındırıcı zarfı (envelope) yazma yolunda gerekmez, platform
297
+ // PATCH ucu akışı tek başına yazar. Net diff okunan kayıt ile son hâl arasından hesaplanır.
298
+ const netOps = fastJsonPatch.compare(current, flow);
299
+ const saved = await platform.tenant('PATCH', project, flowPath(host, applicationId, hostId, flowId), {
300
+ environment,
301
+ body: { baseVersion: baseVersionOf(current, 'flow_get'), ops: netOps }
302
+ });
303
+ const written = patchResult(saved, flow);
304
+ const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, flowId, { steps: flow.steps, errorSteps: flow.errorSteps });
305
+ return attachFindings(withWriteDiff(withNormalization({
306
+ host,
307
+ hostId,
308
+ applied: operations.length,
309
+ newVersion: written.newVersion,
310
+ patchTargets: resolved.targets,
311
+ flow: flowSummary(written.data),
312
+ ...(parameterWarnings.length > 0 ? { parameterWarnings } : {})
313
+ }, [...normalized, ...resolved.notes.map(note => `patch yolu seçiciden indekse çözüldü: ${note}`)]), drops), findings);
215
314
  }));
216
315
  server.registerTool('flow_delete', {
217
316
  title: 'Akış sil',
@@ -219,7 +318,7 @@ export function registerFlowTools(server, platform) {
219
318
  inputSchema: { project: projectParam, environment: environmentParam, host: hostParam, applicationId: guid, hostId: guid, flowId: flowIdParam },
220
319
  annotations: destructive
221
320
  }, handle(config, async ({ project, environment, host, applicationId, hostId, flowId }) => {
222
- const envelope = await hostEnvelope(platform, host, project, environment, applicationId, hostId);
321
+ const { envelope } = await hostEnvelope(platform, host, project, environment, applicationId, hostId);
223
322
  await platform.tenant('DELETE', project, flowPath(host, applicationId, hostId, flowId), { environment, body: { id: flowId, ...envelope } });
224
323
  return { deleted: flowId, host, hostId };
225
324
  }));
@@ -1,6 +1,8 @@
1
1
  import { guid, projectParam, environmentParam, definitionParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
2
2
  import { handle } from '../platform/responses.js';
3
3
  import { z } from 'zod';
4
+ import { evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/policy.js';
5
+ import { policyRules } from '../knowledge/rules.js';
4
6
  export function registerFolderTools(server, platform) {
5
7
  const config = platform.config;
6
8
  server.registerTool('folder_list', {
@@ -17,10 +19,13 @@ export function registerFolderTools(server, platform) {
17
19
  }, handle(config, ({ project, environment, id, breadcrumb }) => platform.tenant('GET', project, breadcrumb ? `/folders/${id}/breadcrumb` : `/folders/${id}`, { environment })));
18
20
  server.registerTool('folder_create', {
19
21
  title: 'Klasör oluştur',
20
- description: "Yeni klasör oluşturur (definition = CreateFolderCommand: { name, parentId? }). DİKKAT: fiziksel klasör adı ve depolama yolu ASCII'ye katlanır ('Donanım' → 'Donanim') ve aynı üst klasörde çakışırsa sonuna ' (1)' eklenir — kullanıcıya gösterilen displayName ham adı korur. Yanıttaki name/path katlanmış hâldir, dosya yollarını oradan okuyun.",
22
+ description: "Yeni klasör oluşturur (definition = CreateFolderCommand: { name, parentId? }). DİKKAT: fiziksel klasör adı ve depolama yolu ASCII'ye katlanır ('Donanım' → 'Donanim') ve aynı üst klasörde çakışırsa sonuna ' (1)' eklenir — kullanıcıya gösterilen displayName ham adı korur. Yanıttaki name/path katlanmış hâldir, dosya yollarını oradan okuyun. Backend ön kontrolü (FLD-001, block): name zorunlu ve \\|!#~$%&/=?»«@£§€{};'<>, karakterlerini içeremez.",
21
23
  inputSchema: { project: projectParam, environment: environmentParam, definition: definitionParam },
22
24
  annotations: writes
23
- }, handle(config, ({ project, environment, definition }) => platform.tenant('POST', project, '/folders', { environment, body: definition })));
25
+ }, handle(config, async ({ project, environment, definition }) => {
26
+ const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'folder', definition, { operation: 'create' }));
27
+ return attachFindings(await platform.tenant('POST', project, '/folders', { environment, body: definition }), findings);
28
+ }));
24
29
  server.registerTool('folder_update', {
25
30
  title: 'Klasör yetkilerini güncelle',
26
31
  description: "Klasörün YETKİLENDİRMESİNİ günceller — `UpdateFolderCommand`'ın alanları yalnız `middlewareIds` ve `roleIds`'tir. Ad değiştirmek için folder_rename kullanın; **klasör TAŞIMA (parentId değiştirme) platformda desteklenmiyor** — gövdeye parentId/name koyarsanız sessizce yok sayılır ve istek yine 200 döner (etkinlik turu Ç3).",
@@ -0,0 +1,63 @@
1
+ import { guid, projectParam, environmentParam, readOnly, writes, updates, destructive } from './common.js';
2
+ import { handle } from '../platform/responses.js';
3
+ import { z } from 'zod';
4
+ const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
5
+ const nameParam = z.string().min(1).regex(identifier, 'Geçerli bir JavaScript tanımlayıcısı olmalı')
6
+ .describe("Motorun bağlayacağı ad; ifadelerde bu adla çağrılır (sum(1, 2)). Sistem fonksiyonlarının adları (Now, UtcNow, GenerateGuid, crypto…) rezervedir ve reddedilir");
7
+ const functionParam = z.string().min(1)
8
+ .describe("Klasik ya da arrow JavaScript FONKSİYON İFADESİ: '(left, right) => left + right' ya da 'function (a) { return a * 2 }'. Deyim değil ifade olmalı — 'return a + b' gibi bir gövde tek başına reddedilir. Platform kaydetmeden önce gerçek bir Jint motorunda çalıştırıp sonucun çağrılabilir olduğunu doğrular; SÖZDİZİMİ ve çağrılabilirlik denetlenir, gövdenin içindeki serbest değişkenler DENETLENMEZ");
9
+ const descriptionParam = z.string().min(1)
10
+ .describe('Fonksiyonun ne işe yaradığı; Studio kod editöründe autocomplete infobox başlığı olarak görünür. Zorunludur');
11
+ const schemaParam = z.array(z.object({
12
+ name: z.string().min(1).regex(identifier, 'Geçerli bir JavaScript tanımlayıcısı olmalı')
13
+ .describe('Parametre adı; fonksiyon içinde benzersiz olmalı'),
14
+ description: z.string().min(1).describe('Parametrenin ne olduğu; infobox\'ta parametre satırı olarak görünür. Zorunludur')
15
+ })).describe('Parametre şeması, SIRALI. Yalnız dokümantasyon ve autocomplete imzası için kullanılır — motor çağrıyı bu şemaya göre DOĞRULAMAZ, eksik argüman undefined gelir. Parametresiz fonksiyonda boş dizi verin');
16
+ export function registerGlobalFunctionTools(server, platform) {
17
+ const config = platform.config;
18
+ server.registerTool('global_function_list', {
19
+ title: 'Global fonksiyonları listele',
20
+ description: "Tenant kapsamındaki global JavaScript fonksiyonlarını listeler. Bunlar her workflow/automation/middleware koşumunda ve Studio preview'ında motora bağlanır; ifadelerde çıplak adla çağrılırlar.",
21
+ inputSchema: { project: projectParam, environment: environmentParam },
22
+ annotations: readOnly
23
+ }, handle(config, ({ project, environment }) => platform.tenant('GET', project, '/global-functions', { environment })));
24
+ server.registerTool('global_function_get', {
25
+ title: 'Global fonksiyonu getir',
26
+ description: 'Tek bir global fonksiyonun adını, gövdesini, açıklamasını ve parametre şemasını getirir.',
27
+ inputSchema: { project: projectParam, environment: environmentParam, id: guid },
28
+ annotations: readOnly
29
+ }, handle(config, ({ project, environment, id }) => platform.tenant('GET', project, `/global-functions/${id}`, { environment })));
30
+ server.registerTool('global_function_create', {
31
+ title: 'Global fonksiyon tanımla',
32
+ description: "Yeni global fonksiyon kaydeder. Ad tenant içinde BENZERSİZDİR. Fonksiyon açıklaması ve HER parametre açıklaması zorunludur — Studio bunları autocomplete infobox'ında gösterir. Gövde bağlamdan bağımsız olmalı: içinde 'input' ya da bir adım adı gibi SERBEST DEĞİŞKEN kullanırsanız kayıt geçer ama fonksiyon o değişkenin tanımlı olmadığı her akışta 'X is not defined' ile patlar; dinamik veriyi parametre olarak geçirin.",
33
+ inputSchema: {
34
+ project: projectParam,
35
+ environment: environmentParam,
36
+ name: nameParam,
37
+ function: functionParam,
38
+ description: descriptionParam,
39
+ schema: schemaParam
40
+ },
41
+ annotations: writes
42
+ }, handle(config, ({ project, environment, name, function: body, description, schema }) => platform.tenant('POST', project, '/global-functions', { environment, body: { name, function: body, description, schema } })));
43
+ server.registerTool('global_function_update', {
44
+ title: 'Global fonksiyonu güncelle',
45
+ description: "Global fonksiyonun tamamını değiştirir (PUT — dört alanı da gönderin). ADI DEĞİŞTİRMEK, fonksiyonu çağıran bir workflow varsa 400 ile reddedilir (CantRenameUsedGlobalFunctionException); gövde/açıklama/şema değişikliği serbesttir. Adı gerçekten değiştirmeniz gerekiyorsa önce çağıran akışlardaki ifadeleri güncelleyin.",
46
+ inputSchema: {
47
+ project: projectParam,
48
+ environment: environmentParam,
49
+ id: guid,
50
+ name: nameParam,
51
+ function: functionParam,
52
+ description: descriptionParam,
53
+ schema: schemaParam
54
+ },
55
+ annotations: updates
56
+ }, handle(config, ({ project, environment, id, name, function: body, description, schema }) => platform.tenant('PUT', project, `/global-functions/${id}`, { environment, body: { name, function: body, description, schema } })));
57
+ server.registerTool('global_function_delete', {
58
+ title: 'Global fonksiyonu sil',
59
+ description: "Global fonksiyonu siler. Fonksiyonu çağıran bir workflow varsa 400 ile reddedilir (CantDeleteUsedGlobalFunctionException) ve mesaj çağıran akışların adlarını sayar. Eşleşme tam tanımlayıcı sınırıyla aranır: 'checksum' ya da 'mySum' içeren bir ifade 'sum' çağrısı sayılmaz.",
60
+ inputSchema: { project: projectParam, environment: environmentParam, id: guid },
61
+ annotations: destructive
62
+ }, handle(config, ({ project, environment, id }) => platform.tenant('DELETE', project, `/global-functions/${id}`, { environment })));
63
+ }