@appaflytech/zeyrek-ai-mcp 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.env.example +2 -2
  2. package/.env.production.example +9 -1
  3. package/README.md +14 -38
  4. package/dist/config.js +36 -3
  5. package/dist/knowledge/backendConstraints.js +982 -0
  6. package/dist/knowledge/componentCatalog.js +549 -92
  7. package/dist/knowledge/componentEvents.js +120 -25
  8. package/dist/knowledge/coverage.js +43 -0
  9. package/dist/knowledge/flowGuards.js +30 -0
  10. package/dist/knowledge/functionGuards.js +134 -0
  11. package/dist/knowledge/guideSections.js +49 -0
  12. package/dist/knowledge/hydrate.js +97 -0
  13. package/dist/knowledge/languageCodes.js +36 -0
  14. package/dist/knowledge/languages.js +105 -0
  15. package/dist/knowledge/layoutSummary.js +10 -0
  16. package/dist/knowledge/localization.js +51 -0
  17. package/dist/knowledge/normalize.js +500 -16
  18. package/dist/knowledge/outlines.js +167 -12
  19. package/dist/knowledge/policy.js +14 -5
  20. package/dist/knowledge/propTypes.js +80 -0
  21. package/dist/knowledge/rules.js +961 -44
  22. package/dist/knowledge/schemaGuard.js +199 -0
  23. package/dist/knowledge/styleResolve.js +63 -4
  24. package/dist/observability/trace.js +27 -3
  25. package/dist/platform/csv.js +123 -0
  26. package/dist/platform/errors.js +107 -6
  27. package/dist/platform/http.js +34 -4
  28. package/dist/platform/patch.js +27 -0
  29. package/dist/platform/refs.js +104 -0
  30. package/dist/platform/responses.js +25 -6
  31. package/dist/platform/screenshot.js +544 -16
  32. package/dist/platform/secrets.js +40 -0
  33. package/dist/platform/tokenVault.js +89 -0
  34. package/dist/platform/writeDiff.js +281 -0
  35. package/dist/resources/examples/flow-call-function.json +66 -0
  36. package/dist/resources/examples/function-total.json +70 -0
  37. package/dist/resources/examples/layout-app-shell.json +1 -1
  38. package/dist/resources/examples/microfunction-node-sum.json +56 -0
  39. package/dist/resources/examples/microservice-table-list.json +2 -4
  40. package/dist/resources/examples/page-dynamic-form.json +3 -3
  41. package/dist/resources/examples/page-flex-sections.json +99 -0
  42. package/dist/resources/examples/page-sign-in.json +4 -3
  43. package/dist/resources/guides/component.md +81 -8
  44. package/dist/resources/guides/entity.md +712 -20
  45. package/dist/resources/guides/expressions.md +47 -2
  46. package/dist/resources/guides/external-component.md +9 -0
  47. package/dist/resources/guides/functions.md +262 -0
  48. package/dist/resources/guides/getting-started.md +82 -7
  49. package/dist/resources/guides/layout.md +53 -10
  50. package/dist/resources/guides/merge.md +231 -0
  51. package/dist/resources/guides/microfunction.md +166 -0
  52. package/dist/resources/guides/microservice.md +287 -15
  53. package/dist/resources/guides/middleware.md +89 -1
  54. package/dist/resources/guides/modify-existing.md +23 -0
  55. package/dist/resources/guides/page.md +402 -49
  56. package/dist/resources/guides/translation.md +150 -8
  57. package/dist/resources/guides/view.md +16 -1
  58. package/dist/resources/guides/workflow.md +199 -28
  59. package/dist/resources/knowledge.js +5 -0
  60. package/dist/resources/rules.js +27 -0
  61. package/dist/resources/schema-fingerprint.json +8 -0
  62. package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
  63. package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
  64. package/dist/resources/schemas/CreateBulkRowCommand.json +36 -0
  65. package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
  66. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
  67. package/dist/resources/schemas/CreateEntityCommand.json +52 -12
  68. package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
  69. package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
  70. package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
  71. package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
  72. package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
  73. package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
  74. package/dist/resources/schemas/CreateMicroserviceCommand.json +145 -3
  75. package/dist/resources/schemas/CreateQueryCommand.json +56 -3
  76. package/dist/resources/schemas/CreateRowCommand.json +38 -0
  77. package/dist/resources/schemas/CreateViewCommand.json +13 -10
  78. package/dist/resources/schemas/CreateWorkflowCommand.json +106 -14
  79. package/dist/resources/schemas/DataJobDto.json +267 -0
  80. package/dist/resources/schemas/ExportRowsCommand.json +172 -0
  81. package/dist/resources/schemas/Filter.json +3 -1
  82. package/dist/resources/schemas/GetRowsQuery.json +6 -1
  83. package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
  84. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
  85. package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
  86. package/dist/resources/schemas/ImportPlanDto.json +260 -0
  87. package/dist/resources/schemas/ImportSampleDto.json +90 -0
  88. package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
  89. package/dist/resources/schemas/PreviewMicroserviceCommand.json +135 -1
  90. package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
  91. package/dist/resources/schemas/PreviewWorkflowCommand.json +45 -9
  92. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
  93. package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
  94. package/dist/resources/schemas/RunImportCommand.json +62 -0
  95. package/dist/resources/schemas/StepDto.json +34 -3
  96. package/dist/resources/schemas/UpdateBulkRowCommand.json +36 -0
  97. package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
  98. package/dist/resources/schemas/UpdateEntityCommand.json +52 -11
  99. package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
  100. package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
  101. package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
  102. package/dist/resources/schemas/UpdateMicroserviceCommand.json +147 -4
  103. package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
  104. package/dist/resources/schemas/UpdateRowCommand.json +40 -0
  105. package/dist/resources/schemas/UpdateViewCommand.json +15 -10
  106. package/dist/resources/schemas/UpdateWorkflowCommand.json +106 -14
  107. package/dist/server.js +17 -1
  108. package/dist/tools/access.js +3 -3
  109. package/dist/tools/activities.js +40 -0
  110. package/dist/tools/applications.js +1 -1
  111. package/dist/tools/automations.js +76 -34
  112. package/dist/tools/common.js +55 -0
  113. package/dist/tools/components.js +16 -7
  114. package/dist/tools/datasources.js +83 -21
  115. package/dist/tools/dependencies.js +1 -1
  116. package/dist/tools/deployments.js +34 -2
  117. package/dist/tools/dtos.js +25 -12
  118. package/dist/tools/entities.js +113 -28
  119. package/dist/tools/environments.js +52 -0
  120. package/dist/tools/externalComponents.js +9 -3
  121. package/dist/tools/files.js +57 -42
  122. package/dist/tools/flows.js +205 -13
  123. package/dist/tools/folders.js +7 -2
  124. package/dist/tools/functions.js +331 -0
  125. package/dist/tools/globalFunctions.js +63 -0
  126. package/dist/tools/importSchemas.js +79 -0
  127. package/dist/tools/imports.js +241 -0
  128. package/dist/tools/layouts.js +103 -29
  129. package/dist/tools/merges.js +113 -0
  130. package/dist/tools/microfunctions.js +155 -0
  131. package/dist/tools/microservices.js +270 -40
  132. package/dist/tools/organizations.js +5 -6
  133. package/dist/tools/pages.js +421 -104
  134. package/dist/tools/platformInfo.js +155 -11
  135. package/dist/tools/projects.js +183 -33
  136. package/dist/tools/queries.js +32 -10
  137. package/dist/tools/revisions.js +94 -0
  138. package/dist/tools/rows.js +484 -50
  139. package/dist/tools/screenWrite.js +68 -0
  140. package/dist/tools/screens.js +28 -0
  141. package/dist/tools/templates.js +32 -5
  142. package/dist/tools/translations.js +24 -8
  143. package/dist/tools/views.js +20 -10
  144. package/dist/tools/workflows.js +56 -21
  145. package/package.json +7 -2
@@ -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.',
@@ -0,0 +1,52 @@
1
+ import { branchNamePattern, destructive, guid, isReservedEnvironmentName, readOnly, writes } from './common.js';
2
+ import { PlatformApiError } from '../platform/errors.js';
3
+ import { handle } from '../platform/responses.js';
4
+ import { z } from 'zod';
5
+ /** Aggregate kuralının aynası (`Environment` ctor); gerçek koruma backend'dedir. Kural `common.ts`'te. */
6
+ const branchName = z.string().regex(branchNamePattern)
7
+ .describe("Dal adı: küçük harfle başlar, yalnız küçük harf/rakam/alt çizgi, 3-31 karakter. Bu ad PG şema adı olur.");
8
+ function assertBranchName(name) {
9
+ if (isReservedEnvironmentName(name)) {
10
+ throw new PlatformApiError(`environment_create: '${name}' rezerve bir şema adı — dal adı olarak kullanılamaz. Başka bir ad seçin.`, 400);
11
+ }
12
+ }
13
+ export function registerEnvironmentTools(server, platform) {
14
+ const config = platform.config;
15
+ server.registerTool('environment_list', {
16
+ title: 'Environment listesi',
17
+ description: "Projenin aktif environment'larını döner: önce sabitler (development, production), sonra dallar. Her satırda 'state' vardır (creating | ready | merging | deleting | merged | error); yalnız 'ready' bir dalda çalışılabilir. Dalda çalışmak için tenant tool'larına environment: '<dal adı>' geçin.",
18
+ inputSchema: { projectId: guid },
19
+ annotations: readOnly
20
+ }, handle(config, ({ projectId }) => platform.org('GET', `/environments/${projectId}`)));
21
+ server.registerTool('environment_create', {
22
+ title: 'Dal environment\'ı aç',
23
+ description: "Projede yeni bir dal environment'ı açar: kendi PG şeması kurulur ve development'ın tanımları oraya kopyalanır. Asenkron bir saga'dır — yanıt 202 döner, dal hemen kullanılabilir DEĞİLDİR. includeRecords true ise projenin KENDİ veritabanındaki bütün veri tablolarının satırları da kopyalanır (çok dilli companion tablolar ve junction'lar dahil; files/folders ve audit trail tabloları hariç, dış datasource'lara dokunulmaz) — entity'de kayıt taşıması ayarı açık olmasa bile. Satırlar merge'de geri taşınmaz.",
24
+ inputSchema: { projectId: guid, name: branchName, includeRecords: z.boolean().optional().describe('Projenin kendi veritabanındaki bütün tabloların satırları da kopyalansın mı (varsayılan false)') },
25
+ annotations: writes
26
+ }, handle(config, async ({ projectId, name, includeRecords }) => {
27
+ assertBranchName(name);
28
+ const payload = await platform.org('POST', `/environments/${projectId}`, { body: { name, includeRecords: includeRecords === true } });
29
+ return {
30
+ ...(payload && typeof payload === 'object' ? payload : { data: payload }),
31
+ note: "dal asenkron açılıyor; environment_list ile state=ready bekleyin"
32
+ };
33
+ }));
34
+ server.registerTool('environment_delete', {
35
+ title: 'Dal environment\'ını sil',
36
+ description: "Bir dal environment'ını ve PG şemasını siler. Sabit environment'lar (development, production) silinemez — gateway bunu HTTP'den önce reddeder. Yalnız 'ready' ya da 'error' durumundaki dal silinebilir; silme asenkrondur.",
37
+ inputSchema: { projectId: guid, environmentId: guid },
38
+ annotations: destructive
39
+ }, handle(config, async ({ projectId, environmentId }) => {
40
+ const environments = await platform.org('GET', `/environments/${projectId}`);
41
+ const rows = environments?.data ?? [];
42
+ const target = rows.find(row => row.id === environmentId);
43
+ if (target && target.isBranch !== true) {
44
+ throw new PlatformApiError(`environment_delete: '${String(target.name)}' sabit bir environment — silinemez. Yalnız dallar silinir (environment_list çıktısında isBranch: true olanlar).`, 400);
45
+ }
46
+ const payload = await platform.org('DELETE', `/environments/${projectId}/${environmentId}`);
47
+ return {
48
+ ...(payload && typeof payload === 'object' ? payload : { data: payload }),
49
+ note: "şema asenkron düşürülüyor; environment_list ile dalın listeden çıkmasını bekleyin"
50
+ };
51
+ }));
52
+ }
@@ -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',
@@ -35,6 +38,56 @@ const mimeTypes = {
35
38
  '.xml': 'application/xml',
36
39
  '.zip': 'application/zip'
37
40
  };
41
+ export async function uploadFile(platform, { project, environment, path, content, text, filename, contentType, folderId }) {
42
+ const given = [path, content, text].filter(value => value !== undefined);
43
+ if (given.length !== 1) {
44
+ 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).");
45
+ }
46
+ let bytes;
47
+ let name;
48
+ if (path) {
49
+ const absolute = resolve(path);
50
+ try {
51
+ bytes = await readFile(absolute);
52
+ }
53
+ catch (error) {
54
+ throw new PlatformApiError(`file_upload: dosya okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`);
55
+ }
56
+ name = filename ?? basename(absolute);
57
+ }
58
+ else if (text !== undefined) {
59
+ if (!filename)
60
+ throw new PlatformApiError('file_upload: `text` verildiğinde `filename` zorunludur.');
61
+ bytes = Buffer.from(text, 'utf8');
62
+ if (bytes.byteLength > maxTextBytes) {
63
+ 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.`);
64
+ }
65
+ name = filename;
66
+ }
67
+ else {
68
+ if (!filename)
69
+ throw new PlatformApiError('file_upload: `content` verildiğinde `filename` zorunludur.');
70
+ bytes = Buffer.from(content, 'base64');
71
+ if (bytes.byteLength === 0)
72
+ throw new PlatformApiError('file_upload: `content` geçerli base64 değil ya da boş.');
73
+ if (bytes.byteLength > maxInlineBytes) {
74
+ throw new PlatformApiError(`file_upload: base64 içerik ${maxInlineBytes} bayt sınırını aşıyor (${bytes.byteLength} B). Büyük dosyalar için \`path\` kullanın.`);
75
+ }
76
+ name = filename;
77
+ }
78
+ if (bytes.byteLength > maxUploadBytes) {
79
+ throw new PlatformApiError(`file_upload: dosya ${maxUploadBytes} bayt platform sınırını aşıyor (${bytes.byteLength} B).`);
80
+ }
81
+ const offendingChar = [...forbiddenNameChars].find(character => name.includes(character));
82
+ if (offendingChar) {
83
+ throw new PlatformApiError(`file_upload: dosya adı '${offendingChar}' karakterini içeriyor; platform doğrulaması bunu 422 ile reddeder (FILE-001, UploadFileCommandValidator). Yasak karakterler: ${forbiddenNameChars}`);
84
+ }
85
+ const resolvedType = contentType ?? mimeTypes[extname(name).toLowerCase()] ?? 'application/octet-stream';
86
+ const multipart = folderId
87
+ ? [{ name: 'FolderId', value: folderId }, { name: 'File', filename: name, contentType: resolvedType, content: bytes }]
88
+ : [{ name: 'File', filename: name, contentType: resolvedType, content: bytes }];
89
+ return platform.tenant('POST', project, '/files/upload', { environment, multipart });
90
+ }
38
91
  export function registerFileTools(server, platform) {
39
92
  const config = platform.config;
40
93
  server.registerTool('file_list', {
@@ -51,57 +104,19 @@ export function registerFileTools(server, platform) {
51
104
  }, handle(config, ({ project, environment, id }) => platform.tenant('GET', project, `/files/${id}`, { environment })));
52
105
  server.registerTool('file_upload', {
53
106
  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.",
107
+ 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
108
  inputSchema: {
56
109
  project: projectParam,
57
110
  environment: environmentParam,
58
111
  path: z.string().min(1).optional().describe("Gateway sürecinin okuyabildiği yerel dosya yolu; `content` ile birlikte verilmez"),
59
112
  content: z.string().min(1).optional().describe('Base64 kodlanmış dosya içeriği; `filename` ile birlikte zorunludur'),
113
+ 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
114
  filename: z.string().min(1).optional().describe("`content` ile zorunlu; `path` verildiğinde dosya adını ezer"),
61
115
  contentType: z.string().min(1).optional().describe('Belirtilmezse uzantıdan çözülür, bilinmeyen uzantıda application/octet-stream'),
62
116
  folderId: guid.optional().describe('Hedef klasör (folder_list); boşsa kök')
63
117
  },
64
118
  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).");
68
- }
69
- let bytes;
70
- let name;
71
- if (path) {
72
- const absolute = resolve(path);
73
- try {
74
- bytes = await readFile(absolute);
75
- }
76
- catch (error) {
77
- throw new PlatformApiError(`file_upload: dosya okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`);
78
- }
79
- name = filename ?? basename(absolute);
80
- }
81
- else {
82
- if (!filename)
83
- throw new PlatformApiError('file_upload: `content` verildiğinde `filename` zorunludur.');
84
- bytes = Buffer.from(content, 'base64');
85
- if (bytes.byteLength === 0)
86
- throw new PlatformApiError('file_upload: `content` geçerli base64 değil ya da boş.');
87
- if (bytes.byteLength > maxInlineBytes) {
88
- throw new PlatformApiError(`file_upload: base64 içerik ${maxInlineBytes} bayt sınırını aşıyor (${bytes.byteLength} B). Büyük dosyalar için \`path\` kullanın.`);
89
- }
90
- name = filename;
91
- }
92
- if (bytes.byteLength > maxUploadBytes) {
93
- throw new PlatformApiError(`file_upload: dosya ${maxUploadBytes} bayt platform sınırını aşıyor (${bytes.byteLength} B).`);
94
- }
95
- const offendingChar = [...forbiddenNameChars].find(character => name.includes(character));
96
- if (offendingChar) {
97
- throw new PlatformApiError(`file_upload: dosya adı '${offendingChar}' karakterini içeriyor; platform doğrulaması bunu reddeder. Yasak karakterler: ${forbiddenNameChars}`);
98
- }
99
- const resolvedType = contentType ?? mimeTypes[extname(name).toLowerCase()] ?? 'application/octet-stream';
100
- const multipart = folderId
101
- ? [{ name: 'FolderId', value: folderId }, { name: 'File', filename: name, contentType: resolvedType, content: bytes }]
102
- : [{ name: 'File', filename: name, contentType: resolvedType, content: bytes }];
103
- return platform.tenant('POST', project, '/files/upload', { environment, multipart });
104
- }));
119
+ }, handle(config, (input) => uploadFile(platform, input)));
105
120
  server.registerTool('file_download', {
106
121
  title: 'Dosya indir',
107
122
  description: "Dosyanın içeriğini indirir ve `outPath` ile verilen yerel yola yazar; dönen gövde yalnız özet (yol, boyut, tip) — ikili içerik model bağlamına girmez. Yol gateway sürecinin çalıştığı makineye göre çözülür.",