@appaflytech/zeyrek-ai-mcp 0.0.6 → 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 (91) hide show
  1. package/.env.example +2 -2
  2. package/.env.production.example +8 -0
  3. package/README.md +9 -8
  4. package/dist/config.js +18 -3
  5. package/dist/knowledge/backendConstraints.js +85 -0
  6. package/dist/knowledge/componentCatalog.js +357 -54
  7. package/dist/knowledge/componentEvents.js +120 -25
  8. package/dist/knowledge/functionGuards.js +134 -0
  9. package/dist/knowledge/normalize.js +53 -2
  10. package/dist/knowledge/outlines.js +31 -0
  11. package/dist/knowledge/policy.js +14 -5
  12. package/dist/knowledge/rules.js +343 -1
  13. package/dist/platform/csv.js +123 -0
  14. package/dist/platform/errors.js +62 -3
  15. package/dist/platform/http.js +1 -0
  16. package/dist/platform/responses.js +16 -4
  17. package/dist/platform/screenshot.js +18 -1
  18. package/dist/resources/examples/flow-call-function.json +66 -0
  19. package/dist/resources/examples/function-total.json +70 -0
  20. package/dist/resources/examples/microfunction-node-sum.json +56 -0
  21. package/dist/resources/examples/page-dynamic-form.json +1 -1
  22. package/dist/resources/examples/page-sign-in.json +1 -1
  23. package/dist/resources/guides/component.md +13 -1
  24. package/dist/resources/guides/entity.md +321 -17
  25. package/dist/resources/guides/expressions.md +3 -0
  26. package/dist/resources/guides/functions.md +262 -0
  27. package/dist/resources/guides/getting-started.md +5 -0
  28. package/dist/resources/guides/merge.md +231 -0
  29. package/dist/resources/guides/microfunction.md +166 -0
  30. package/dist/resources/guides/microservice.md +61 -2
  31. package/dist/resources/guides/modify-existing.md +23 -0
  32. package/dist/resources/guides/page.md +20 -4
  33. package/dist/resources/guides/translation.md +24 -0
  34. package/dist/resources/guides/workflow.md +1 -1
  35. package/dist/resources/schema-fingerprint.json +4 -4
  36. package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
  37. package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
  38. package/dist/resources/schemas/CreateBulkRowCommand.json +3 -0
  39. package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
  40. package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
  41. package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
  42. package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
  43. package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
  44. package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
  45. package/dist/resources/schemas/CreateMicroserviceCommand.json +119 -1
  46. package/dist/resources/schemas/CreateWorkflowCommand.json +13 -2
  47. package/dist/resources/schemas/DataJobDto.json +267 -0
  48. package/dist/resources/schemas/ExportRowsCommand.json +172 -0
  49. package/dist/resources/schemas/Filter.json +3 -1
  50. package/dist/resources/schemas/GetRowsQuery.json +3 -1
  51. package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
  52. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
  53. package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
  54. package/dist/resources/schemas/ImportPlanDto.json +260 -0
  55. package/dist/resources/schemas/ImportSampleDto.json +90 -0
  56. package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
  57. package/dist/resources/schemas/PreviewMicroserviceCommand.json +119 -1
  58. package/dist/resources/schemas/PreviewWorkflowCommand.json +13 -2
  59. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
  60. package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
  61. package/dist/resources/schemas/RunImportCommand.json +62 -0
  62. package/dist/resources/schemas/StepDto.json +13 -2
  63. package/dist/resources/schemas/UpdateBulkRowCommand.json +3 -0
  64. package/dist/resources/schemas/UpdateEntityCommand.json +3 -1
  65. package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
  66. package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
  67. package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
  68. package/dist/resources/schemas/UpdateMicroserviceCommand.json +119 -1
  69. package/dist/resources/schemas/UpdateWorkflowCommand.json +13 -2
  70. package/dist/server.js +15 -3
  71. package/dist/tools/activities.js +40 -0
  72. package/dist/tools/automations.js +14 -5
  73. package/dist/tools/common.js +16 -0
  74. package/dist/tools/dependencies.js +1 -1
  75. package/dist/tools/environments.js +52 -0
  76. package/dist/tools/files.js +51 -50
  77. package/dist/tools/flows.js +95 -2
  78. package/dist/tools/functions.js +331 -0
  79. package/dist/tools/importSchemas.js +79 -0
  80. package/dist/tools/imports.js +241 -0
  81. package/dist/tools/merges.js +113 -0
  82. package/dist/tools/microfunctions.js +155 -0
  83. package/dist/tools/microservices.js +42 -3
  84. package/dist/tools/pages.js +62 -10
  85. package/dist/tools/platformInfo.js +4 -1
  86. package/dist/tools/revisions.js +94 -0
  87. package/dist/tools/rows.js +342 -22
  88. package/dist/tools/screenWrite.js +11 -2
  89. package/dist/tools/templates.js +32 -5
  90. package/dist/tools/workflows.js +24 -6
  91. package/package.json +3 -1
@@ -7,6 +7,12 @@ import { attachFindings, evaluatePolicy, enforcePolicy } from '../knowledge/poli
7
7
  import { policyRules } from '../knowledge/rules.js';
8
8
  import { languageHeader } from '../knowledge/languageCodes.js';
9
9
  import { handle } from '../platform/responses.js';
10
+ import { uploadFile } from './files.js';
11
+ import { PlatformApiError } from '../platform/errors.js';
12
+ import { secretKeyPattern } from '../platform/secrets.js';
13
+ import { csvRows } from '../platform/csv.js';
14
+ import { readFile, stat, writeFile } from 'node:fs/promises';
15
+ import { extname, resolve } from 'node:path';
10
16
  import { z } from 'zod';
11
17
  const rowValues = z.record(z.unknown()).describe("Alan SLUG'ı → değer sözlüğü; anahtarlar entity_get çıktısındaki field SLUG'larıdır (name değil — name kullanmak 'Please enter a valid value' hatası verir)");
12
18
  /**
@@ -25,10 +31,178 @@ function derivedRowFindings(rows) {
25
31
  return [{
26
32
  ruleId: 'ROW-001',
27
33
  severity: 'warn',
28
- message: `Gövdede ${rows.length} satır var. Bu satırlar VAR OLAN satırlardan türetiliyorsa (geçmiş/hareket/log/junction tablosu, backfill) onları gövdede taşımayın: tek bir kayıtlı 'INSERT … SELECT' sorgusu aynı işi TEK çağrıda yapar ve hiçbir id elle taşınmaz (ölçüldü 2026-09-07). Satırlar dışarıdan geliyorsa (içe aktarma, kullanıcı verisi) bu çağrı doğru yoldur. Ham SQL yolu UYGULAMA katmanını atlar (automation tetiklenmez, incremental sayaç ilerlemez, localization companion yazılmaz) ama veritabanı kısıtları — NOT NULL, FK, UNIQUE, enum CHECK — aynen uygulanır. Reçete, merdiven kalıbı ve anahtar sözleşmesi: platform_guide('entity') §'Toplu ve türetilmiş satır'.`,
34
+ message: `Gövdede ${rows.length} satır var. Satırlar dışarıdan bir dosyadan geliyorsa gövdeden değil DOSYADAN yazın: aynı çağrıyı \`path: "seed.csv" | "seed.json"\` ile verin, gateway dosyayı okur ve 1000'lik partilere böler — satırlar ajan bağlamından hiç geçmez. Bu satırlar VAR OLAN satırlardan türetiliyorsa (geçmiş/hareket/log/junction tablosu, backfill) onları gövdede taşımayın: tek bir kayıtlı 'INSERT … SELECT' sorgusu aynı işi TEK çağrıda yapar ve hiçbir id elle taşınmaz (ölçüldü 2026-09-07). Satırlar dışarıdan geliyorsa (içe aktarma, kullanıcı verisi) bu çağrı doğru yoldur. Ham SQL yolu UYGULAMA katmanını atlar (automation tetiklenmez, incremental sayaç ilerlemez, localization companion yazılmaz) ama veritabanı kısıtları — NOT NULL, FK, UNIQUE, enum CHECK — aynen uygulanır. Reçete, merdiven kalıbı ve anahtar sözleşmesi: platform_guide('entity') §'Toplu ve türetilmiş satır'.`,
29
35
  guide: 'ngapps://guides/entity'
30
36
  }];
31
37
  }
38
+ const bulkBatchSize = 1000;
39
+ const maxPathBytes = 50 * 1024 * 1024;
40
+ const maxPathRows = 100_000;
41
+ const pathParam = z.string().min(1).optional()
42
+ .describe("Gateway sürecinin okuyabildiği yerel `.json` (satır dizisi) ya da `.csv` dosyası; 'row'/'rows' ile birlikte verilmez. Satırlar ajan bağlamından HİÇ geçmez: gateway dosyayı okur, 1000'lik partilere böler ve ardışık toplu çağrı yapar. CSV ayracı ilk satırdan bulunur (`;` sayısı `,` sayısını geçerse `;`), değerler string gider ve tipe backend çevirir. 50 MB / 100.000 satır üstü reddedilir — o boyut import_analyze zinciridir.");
43
+ const relationsByNameParam = z.boolean().optional()
44
+ .describe("İlişki alanlarına id yerine GÖRÜNEN AD yazmayı açar: 'departman: \"Satış\"' hedef tablonun display ya da benzersiz alanında aranır ve id'ye çevrilir. Yalnız toplu yolda (rows / path) çalışır. Eşleşmeyen değer 422 döndürür ve hangi satırın hangi alanı olduğunu söyler — sessizce boş bırakmaz.");
45
+ function collectIds(payload, into) {
46
+ const data = payload?.data;
47
+ if (Array.isArray(data))
48
+ into.push(...data);
49
+ }
50
+ async function readRowsFromPath(tool, path) {
51
+ const absolute = resolve(path);
52
+ const extension = extname(absolute).toLowerCase();
53
+ if (extension !== '.json' && extension !== '.csv') {
54
+ throw new PlatformApiError(`${tool}: \`path\` yalnız .json ya da .csv okur — verilen: ${absolute}`);
55
+ }
56
+ let sizeBytes;
57
+ try {
58
+ sizeBytes = (await stat(absolute)).size;
59
+ }
60
+ catch (error) {
61
+ throw new PlatformApiError(`${tool}: dosya okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`);
62
+ }
63
+ if (sizeBytes > maxPathBytes) {
64
+ throw new PlatformApiError(`${tool}: dosya ${maxPathBytes} bayt sınırını aşıyor (${sizeBytes} B). Bu boyut satır yazma yolu değil içe aktarma zinciridir — import_analyze → import_plan → import_run.`);
65
+ }
66
+ const text = await readFile(absolute, 'utf8');
67
+ let rows;
68
+ if (extension === '.csv') {
69
+ rows = csvRows(text);
70
+ }
71
+ else {
72
+ try {
73
+ rows = JSON.parse(text);
74
+ }
75
+ catch (error) {
76
+ throw new PlatformApiError(`${tool}: JSON okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}).`);
77
+ }
78
+ }
79
+ if (!Array.isArray(rows)) {
80
+ throw new PlatformApiError(`${tool}: \`path\` dosyası bir satır DİZİSİ taşımalı — ${absolute} içinde ${typeof rows} bulundu.`);
81
+ }
82
+ if (rows.length === 0) {
83
+ throw new PlatformApiError(`${tool}: dosyada satır yok — ${absolute}. CSV ise başlık satırı var mı ve ayraç doğru mu kontrol edin.`);
84
+ }
85
+ if (rows.length > maxPathRows) {
86
+ throw new PlatformApiError(`${tool}: ${rows.length} satır, ${maxPathRows} sınırının üstünde. Bu boyutta içe aktarma zincirini kullanın: import_analyze → import_plan → import_run (iş kuyruğa alınır, ilerleme import_status'tan izlenir).`);
87
+ }
88
+ return rows;
89
+ }
90
+ /**
91
+ * Dosya satır numarası 1 tabanlıdır ve başlık satırını sayar: CSV'de düşen satırı kullanıcı
92
+ * editörde bu numarayla bulur. JSON dizisinde de aynı formül kullanılır — ilk eleman 2. satırda
93
+ * durur çünkü açılış köşeli parantezi ilk satırı yer.
94
+ */
95
+ function fileLineOf(batchIndex, rowIndexInBatch) {
96
+ return batchIndex * bulkBatchSize + rowIndexInBatch + 2;
97
+ }
98
+ async function writeInBatches(platform, method, project, environment, headers, entityId, rows, rowContext, relationsByName) {
99
+ const query = relationsByName ? { relationsByName: true } : undefined;
100
+ const ids = [];
101
+ let written = 0;
102
+ let batches = 0;
103
+ for (let start = 0; start < rows.length; start += bulkBatchSize) {
104
+ const batch = rows.slice(start, start + bulkBatchSize);
105
+ const batchIndex = batches;
106
+ try {
107
+ const payload = await platform.tenant(method, project, `/entities/${entityId}/row/bulk`, { environment, headers, body: { rows: batch, ...(rowContext ? { rowContext } : {}) }, ...(query ? { query } : {}) });
108
+ collectIds(payload, ids);
109
+ written += batch.length;
110
+ batches += 1;
111
+ }
112
+ catch (error) {
113
+ // Hiç satır yazılmadıysa ortada kısmi durum yoktur: bu düz bir REDDİR ve platformun kendi
114
+ // mesajı (satır indeksi + all-or-nothing notu) her özetten iyidir. Yutup 'written: 0'
115
+ // döndürmek çağırana başarı gibi okunurdu.
116
+ if (written === 0)
117
+ throw error;
118
+ const rowIndexInBatch = error instanceof PlatformApiError && typeof error.rowIndex === 'number' ? error.rowIndex : 0;
119
+ return {
120
+ written,
121
+ batches,
122
+ ...(ids.length > 0 ? { ids } : {}),
123
+ stopped: {
124
+ fileLine: fileLineOf(batchIndex, rowIndexInBatch),
125
+ batchIndex,
126
+ rowIndexInBatch,
127
+ error: error instanceof Error ? error.message : String(error),
128
+ note: `${written} satır YAZILDI ve öyle kaldı: partiler arası transaction yoktur, yalnız düşen parti geri alındı. Dosyayı ${fileLineOf(batchIndex, rowIndexInBatch)}. satırdan düzeltip kalanını yeniden gönderin; hepsi-ya-hiç gerekiyorsa import_run kullanın.`
129
+ }
130
+ };
131
+ }
132
+ }
133
+ return { written, batches, ...(ids.length > 0 ? { ids } : {}) };
134
+ }
135
+ /**
136
+ * Üç kaynaktan tam olarak biri: gövdedeki tek satır, gövdedeki dizi ya da dosya. İkisi birden
137
+ * verilirse hangisinin yazılacağı belirsizdir ve sessizce birini seçmek yanlış veri yazar.
138
+ */
139
+ function assertOneSource(tool, row, rows, path) {
140
+ const given = [row, rows, path].filter(value => value !== undefined).length;
141
+ if (given === 1)
142
+ return;
143
+ throw new Error(given === 0
144
+ ? `${tool}: 'row', 'rows' ya da 'path' parametrelerinden biri zorunlu.`
145
+ : `${tool}: 'row', 'rows' ve 'path' birlikte verilemez — tam olarak biri.`);
146
+ }
147
+ const exportFolderName = 'Dışa aktarmalar';
148
+ const maxSelectedRowIds = 1000;
149
+ /**
150
+ * `inline` örneği ajan bağlamına girer. Sütun tanımı + iki satır dar bir tabloda birkaç KB'dir;
151
+ * yüzlerce sütunlu tabloda aynı iki satır bağlamı dosyanın kendisi kadar doldururdu. Sınırı aşan
152
+ * örnekte `data` düşer, `columns` kalır — dosyanın şekli yine görülür, satırlar Sürücü'dedir.
153
+ */
154
+ const templateInlineMaxChars = 16_000;
155
+ const templateFormatNotes = {
156
+ xlsx: "xlsx şablonda ipuçları başlık hücresi yorumunda ve gizli `_zeyrek` sayfasındadır; veri alanında ipucu satırı yoktur.",
157
+ csv: 'csv şablonda ipucu satırı ve `#zeyrek` satırı yoktur; başlıklar alan adıdır.',
158
+ json: 'JSON şablon `{ meta, data }` gövdesidir; `meta` analizde atlanır, satırlar `data`dadır ve anahtarları alan slug\'ıdır.'
159
+ };
160
+ const rowFiltersParam = z.array(z.record(z.unknown())).optional()
161
+ .describe('Platform Filter dizisi; biçim için row_list\'i filtresiz çağırıp Studio\'daki bir filtreyi referans alın');
162
+ const exportScopeParam = z.object({
163
+ kind: z.enum(['view', 'all', 'selected']).describe("'view' filtre + sıralama ile, 'all' tüm satırlar, 'selected' rowIds listesi"),
164
+ filters: rowFiltersParam,
165
+ sort: z.object({ field: z.string(), ascending: z.boolean().optional() }).optional()
166
+ .describe("Sıralama; 'ascending' verilmezse artan"),
167
+ rowIds: z.array(z.string()).optional().describe(`'selected' kapsamında dışa aktarılacak satır id'leri; en çok ${maxSelectedRowIds} — üstü için filtre kullanın`)
168
+ }).optional().describe("Verilmezse tüm satırlar ('all')");
169
+ /**
170
+ * Dosya adı sunucudan gelir; başlık RFC 5987 yüzde kodlu da olabilir. Ada zaman damgası
171
+ * eklenir çünkü aynı tablo günde birkaç kez dışa aktarılır ve Sürücü'de aynı ad çakışır.
172
+ */
173
+ function exportFileName(headers, fallbackSlug, extension) {
174
+ const disposition = headers['content-disposition'] ?? '';
175
+ const matched = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition);
176
+ let base = fallbackSlug;
177
+ const captured = matched?.[1];
178
+ if (captured) {
179
+ try {
180
+ base = decodeURIComponent(captured);
181
+ }
182
+ catch {
183
+ base = captured;
184
+ }
185
+ }
186
+ const dot = base.lastIndexOf('.');
187
+ const stem = dot > 0 ? base.slice(0, dot) : base;
188
+ const stamp = new Date().toISOString().slice(0, 16).replace(/[-:]/g, '').replace('T', '-');
189
+ return `${stem}-${stamp}.${extension}`;
190
+ }
191
+ /**
192
+ * Klasörün `name`'i ASCII'ye katlanmıştır ('Dışa aktarmalar' → 'Disa aktarmalar'); ham ad yalnız
193
+ * `displayName`'de durur. Katlanmış ada bakan arama hiçbir zaman eşleşmez ve her dışa aktarma
194
+ * yeni bir '… (1)' klasörü açardı.
195
+ */
196
+ async function exportFolderIdAsync(platform, project, environment) {
197
+ const listed = await platform.tenant('GET', project, '/folders', { environment, query: { searchTerm: exportFolderName, pageLength: 50 } });
198
+ const rows = listed?.data ?? [];
199
+ const existing = rows.find(folder => folder.displayName === exportFolderName);
200
+ if (typeof existing?.id === 'string')
201
+ return existing.id;
202
+ const created = await platform.tenant('POST', project, '/folders', { environment, body: { name: exportFolderName } });
203
+ const id = created?.data?.id;
204
+ return typeof id === 'string' ? id : undefined;
205
+ }
32
206
  const includeTranslationsParam = z.boolean().optional()
33
207
  .describe("Çok dilli alanların TÜM dillerini de getirir: satıra 'localizations: { <dil>: { <alan>: <değer> } }' eklenir. Verilmezse satır yalnız istek dilinde düzleştirilmiş gelir (çevirisi olmayan alan varsayılan dile düşer).");
34
208
  const localizationsParam = z.record(z.record(z.unknown())).optional()
@@ -43,7 +217,7 @@ export function registerRowTools(server, platform) {
43
217
  environment: environmentParam,
44
218
  entityId: entityRef,
45
219
  selectedFields: z.array(z.string()).optional().describe('Dönen satırlar bu alan adlarına indirgenir, id her zaman dahildir; platform tam satırı hesapladığı için kırpma gateway tarafında yapılır'),
46
- filters: z.array(z.record(z.unknown())).optional().describe('Platform Filter dizisi; biçim için row_list\'i filtresiz çağırıp Studio\'daki bir filtreyi referans alın'),
220
+ filters: rowFiltersParam,
47
221
  language: languageParam,
48
222
  includeTranslations: includeTranslationsParam,
49
223
  ...pagingParams
@@ -93,29 +267,34 @@ export function registerRowTools(server, platform) {
93
267
  entityId: entityRef,
94
268
  row: rowValues.optional(),
95
269
  rows: z.array(z.record(z.unknown())).optional(),
270
+ path: pathParam,
271
+ relationsByName: relationsByNameParam,
96
272
  localizations: localizationsParam,
97
273
  language: languageParam,
98
274
  rowContext: z.record(z.unknown()).optional()
99
275
  .describe("Satır olayının DIŞINDAN automation'a geçirilecek değerler. Tetiklenen automation bunları ifadelerinde `rowContext.<anahtar>` ile okur — zaman damgası, göç kaynağı, toplu iş kimliği gibi. Tarihsel (geçmiş tarihli) veri üretmenin yolu budur: automation `UtcNow()` yerine `rowContext.occurredAt`'i yazabilir. Verilmezse automation bağlamı değişmez.")
100
276
  },
101
277
  annotations: writes
102
- }, handle(config, async ({ project, environment, entityId, row, rows, localizations, rowContext, language }) => {
103
- if (row && rows)
104
- throw new Error("'row' ve 'rows' birlikte verilemez.");
278
+ }, handle(config, async ({ project, environment, entityId, row, rows, path, relationsByName, localizations, rowContext, language }) => {
279
+ assertOneSource('row_create', row, rows, path);
105
280
  if (localizations && !row)
106
281
  throw new Error("Üst düzey 'localizations' yalnız 'row' ile verilir: toplu uçta hangi satıra ait olduğu belirsizdir. Toplu eklemede dil değerlerini satırın kendi içine koyun — rows: [{ …, localizations: { tr: {…} } }].");
107
- if (!row && !rows)
108
- throw new Error("'row' ya da 'rows' parametrelerinden biri zorunlu.");
282
+ if (relationsByName && row)
283
+ throw new Error("'relationsByName' yalnız toplu yolda ('rows' ya da 'path') çalışır: tek satır ucu ad → id çözümlemesi yapmaz, ilişki alanına id yazın.");
109
284
  const notes = [];
110
285
  const headers = languageHeader(language, notes);
111
286
  const wantedLocalizations = localizations && Object.keys(localizations).length > 0 ? localizations : undefined;
112
- const shapeFindings = enforcePolicy(evaluatePolicy(config, policyRules, 'row', rows ? { rows } : { row }, { operation: 'create' }));
287
+ const fileRows = path ? await readRowsFromPath('row_create', path) : undefined;
288
+ const bulkRows = rows ?? fileRows;
289
+ const shapeFindings = enforcePolicy(evaluatePolicy(config, policyRules, 'row', bulkRows ? { rows: bulkRows } : { row }, { operation: 'create' }));
113
290
  const resolver = new RefResolver(platform, project, environment);
114
- if (rows) {
291
+ if (bulkRows) {
115
292
  const bulkEntityId = await resolveEntityId(resolver, entityId);
116
- const bulkBody = { rows, ...(rowContext ? { rowContext } : {}) };
117
- const bulkDrift = unmappedMembers('CreateBulkRowCommand', bulkBody);
118
- return attachFindings(withUnmappedMembers(withNormalization(withResolved(await platform.tenant('POST', project, `/entities/${bulkEntityId}/row/bulk`, { environment, headers, body: bulkBody }), resolver.resolved), notes), bulkDrift), [...shapeFindings, ...derivedRowFindings(rows)]);
293
+ const bulkDrift = unmappedMembers('CreateBulkRowCommand', { rows: bulkRows, ...(rowContext ? { rowContext } : {}), ...(relationsByName ? { relationsByName } : {}) });
294
+ const outcome = await writeInBatches(platform, 'POST', project, environment, headers, bulkEntityId, bulkRows, rowContext, relationsByName);
295
+ if (fileRows)
296
+ notes.push(`${path} okundu: ${fileRows.length} satır, ${Math.ceil(fileRows.length / bulkBatchSize)} parti.`);
297
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved(outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
119
298
  }
120
299
  const resolvedEntityId = await resolveEntityId(resolver, entityId);
121
300
  const body = { row, ...(wantedLocalizations ? { localizations: wantedLocalizations } : {}), ...(rowContext ? { rowContext } : {}) };
@@ -158,33 +337,174 @@ export function registerRowTools(server, platform) {
158
337
  entityId: entityRef,
159
338
  row: rowValues.optional(),
160
339
  rows: z.array(z.record(z.unknown())).optional(),
340
+ path: pathParam,
341
+ relationsByName: relationsByNameParam,
161
342
  localizations: localizationsParam,
162
343
  language: languageParam,
163
344
  rowContext: z.record(z.unknown()).optional()
164
345
  .describe("Satır olayının DIŞINDAN automation'a geçirilecek değerler. Tetiklenen automation bunları ifadelerinde `rowContext.<anahtar>` ile okur — zaman damgası, göç kaynağı, toplu iş kimliği gibi. Tarihsel (geçmiş tarihli) veri üretmenin yolu budur: automation `UtcNow()` yerine `rowContext.occurredAt`'i yazabilir. Verilmezse automation bağlamı değişmez.")
165
346
  },
166
347
  annotations: updates
167
- }, handle(config, async ({ project, environment, entityId, row, rows, localizations, rowContext, language }) => {
168
- if (row && rows)
169
- throw new Error("'row' ve 'rows' birlikte verilemez.");
348
+ }, handle(config, async ({ project, environment, entityId, row, rows, path, relationsByName, localizations, rowContext, language }) => {
349
+ assertOneSource('row_update', row, rows, path);
170
350
  if (localizations && !row)
171
351
  throw new Error("Üst düzey 'localizations' yalnız 'row' ile verilir: toplu uçta hangi satıra ait olduğu belirsizdir. Toplu güncellemede dil değerlerini satırın kendi içine koyun — rows: [{ id, localizations: { tr: {…} } }].");
172
- if (!row && !rows)
173
- throw new Error("'row' ya da 'rows' parametrelerinden biri zorunlu.");
174
- const shapeFindings = enforcePolicy(evaluatePolicy(config, policyRules, 'row', rows ? { rows } : { row }, { operation: 'update' }));
352
+ if (relationsByName && row)
353
+ throw new Error("'relationsByName' yalnız toplu yolda ('rows' ya da 'path') çalışır: tek satır ucu ad → id çözümlemesi yapmaz, ilişki alanına id yazın.");
175
354
  const notes = [];
355
+ const fileRows = path ? await readRowsFromPath('row_update', path) : undefined;
356
+ const bulkRows = rows ?? fileRows;
357
+ const shapeFindings = enforcePolicy(evaluatePolicy(config, policyRules, 'row', bulkRows ? { rows: bulkRows } : { row }, { operation: 'update' }));
176
358
  const headers = languageHeader(language, notes);
177
359
  const resolver = new RefResolver(platform, project, environment);
178
360
  const resolvedEntityId = await resolveEntityId(resolver, entityId);
179
- if (rows) {
180
- const bulkBody = { rows, ...(rowContext ? { rowContext } : {}) };
181
- const bulkDrift = unmappedMembers('UpdateBulkRowCommand', bulkBody);
182
- return attachFindings(withUnmappedMembers(withNormalization(withResolved(await platform.tenant('PUT', project, `/entities/${resolvedEntityId}/row/bulk`, { environment, headers, body: bulkBody }), resolver.resolved), notes), bulkDrift), [...shapeFindings, ...derivedRowFindings(rows)]);
361
+ if (bulkRows) {
362
+ const bulkDrift = unmappedMembers('UpdateBulkRowCommand', { rows: bulkRows, ...(rowContext ? { rowContext } : {}), ...(relationsByName ? { relationsByName } : {}) });
363
+ const outcome = await writeInBatches(platform, 'PUT', project, environment, headers, resolvedEntityId, bulkRows, rowContext, relationsByName);
364
+ if (fileRows)
365
+ notes.push(`${path} okundu: ${fileRows.length} satır, ${Math.ceil(fileRows.length / bulkBatchSize)} parti.`);
366
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved(outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
183
367
  }
184
368
  const body = { row, ...(localizations ? { localizations } : {}), ...(rowContext ? { rowContext } : {}) };
185
369
  const drift = unmappedMembers('UpdateRowCommand', body);
186
370
  return attachFindings(withUnmappedMembers(withNormalization(withResolved(await platform.tenant('PUT', project, `/entities/${resolvedEntityId}/row`, { environment, headers, body }), resolver.resolved), notes), drift), shapeFindings);
187
371
  }));
372
+ server.registerTool('row_export', {
373
+ title: 'Satırları dışa aktar',
374
+ description: "Bir tablonun satırlarını xlsx / csv / json dosyasına yazar ve dosyayı projenin SÜRÜCÜSÜNE ('Dışa aktarmalar' klasörü) koyar; yanıt yalnız fileId ve sayılardır — dosya içeriği ajanın bağlamına HİÇ girmez, içeriği görmek için file_download kullanın. Kapsam üç şekildedir: 'view' (filtre + sıralama), 'all', 'selected' (rowIds). 5.000 satırın üstünde platform işi arka plana alır ve 202 ile jobId döner; import_status { jobId } ile izlenir, biten işin dosyası result.reportFileId'dedir. CSV UTF-8 BOM'lu yazılır; ayraç ve ondalık bir yerel ayar profilinden gelir (tr-TR `;` · `1,5`, en-US `,` · `1.5`): dışa aktarmada projenin dili seçer, şablonda 'locale' ile seçilir. 'reimportable: true' dosyaya id sütununu ve _zeyrek meta sayfasını ekler: dosya düzenlenip import_analyze → import_plan { mode: 'upsert', keyField: 'id' } ile geri yüklenebilir (Akış C). 'template: true' tablo satırı yazmaz: tablonun alanlarından İKİ SENTETİK örnek satırlı bir şablon üretir ve değiştirilmeden geri yüklenebilir. 'template: true, inline: true' şablonun şeklini (sütunlar, anahtar önerisi, iki satır) yanıta da koyar — toplu yazımdan önce dosyanın nasıl görünmesi gerektiğini indirmeden görmenin yolu budur; satır satır row_create yerine şablon başlıklarıyla CSV → file_upload { text } → import_analyze → import_plan → import_run, reçete platform_guide('entity') §'Dışa aktarma ve geri yükleme'.",
375
+ inputSchema: {
376
+ project: projectParam,
377
+ environment: environmentParam,
378
+ entityId: entityRef,
379
+ format: z.enum(['xlsx', 'csv', 'json']).optional().describe("Varsayılan 'xlsx'. 'json' iş kuyruğuna alınmaz, satır sayısı ne olursa olsun doğrudan döner."),
380
+ scope: exportScopeParam,
381
+ columns: z.enum(['visible', 'all']).optional().describe("'visible' yalnız selectedFields'ta sayılanları, 'all' sistem alanları dahil hepsini yazar; varsayılan 'all'"),
382
+ selectedFields: z.array(z.string()).optional().describe("columns: 'visible' ile birlikte yazılacak alan SLUG'ları"),
383
+ relations: z.enum(['display', 'id', 'both']).optional().describe("İlişki sütunu görünen adla mı, id ile mi, ikisiyle mi yazılsın; varsayılan 'display'. reimportable ile 'both' zorunludur ve gateway onu kendisi seçer."),
384
+ languages: z.enum(['request', 'all']).optional().describe("'all' çok dilli sütunların TÜM dillerini ayrı sütunlara yazar; varsayılan yalnız istek dili"),
385
+ files: z.enum(['name', 'link']).optional().describe("Dosya sütunu ad olarak mı indirme bağlantısı olarak mı yazılsın; varsayılan 'name'"),
386
+ reimportable: z.boolean().optional().describe("Dosyaya id sütunu ve _zeyrek meta sayfası ekler; ilişki sütunları ad+id olarak yazılır. Geri yükleme yolu: import_analyze → import_plan { mode: 'upsert', keyField: 'id' }."),
387
+ template: z.boolean().optional().describe("Tablonun alanlarından iki örnek satırlı şablon; ipuçları veri alanının dışında (xlsx başlık yorumu + `_zeyrek`, json `meta`, csv'de yok); değiştirilmeden geri yüklenebilir. PK, identity, sistem ve artan kod sütunu yoktur; değerler sentetiktir, ilişki sütunu boş ve `manual`dır. scope/columns/selectedFields/relations/languages/files/reimportable yok sayılır."),
388
+ locale: z.enum(['tr-TR', 'en-US']).optional().describe("Yalnız template ile: csv ayracı, ondalık ve tarih profilini ve inline örneğin yerel ayarını seçer (tr-TR `;` · `1,5` · `dd.MM.yyyy`; en-US `,` · `1.5` · `M/d/yyyy`). Verilmezse projenin varsayılan dili seçer. Dışa aktarmada (template yok) yok sayılır."),
389
+ inline: z.boolean().optional().describe(`Yalnız template: true ile: şablonun şekli yanıta 'sample { entity, locale, keyField, columns: [{ slug, header, type, required, options?, relationEntity?, matchField?, manual? }], data }' olarak da konur — data iki sentetik satırdır (JSON biçimi: ondalık nokta, tarih yyyy-MM-dd). Örnek ${templateInlineMaxChars} karakteri aşarsa data atlanır, columns kalır. template olmadan istek gitmeden reddedilir.`),
390
+ download: z.string().min(1).optional().describe("Ayrıca gateway sürecinin yazabildiği bu yerel yola kaydeder; Sürücü'deki kopya yine oluşur.")
391
+ },
392
+ annotations: writes
393
+ }, handle(config, async ({ project, environment, entityId, format, scope, columns, selectedFields, relations, languages, files, reimportable, template, locale, inline, download }) => {
394
+ if (inline && !template) {
395
+ throw new PlatformApiError('row_export: inline yalnız template: true ile kullanılır — satır içi örnek şablonun şeklidir; tablonun gerçek satırlarını görmek için row_list kullanın.');
396
+ }
397
+ if (scope?.kind === 'selected' && !(scope.rowIds?.length)) {
398
+ throw new PlatformApiError("row_export: scope.kind 'selected' verildiğinde rowIds boş olamaz — hangi satırların yazılacağı belirsizdir.");
399
+ }
400
+ if ((scope?.rowIds?.length ?? 0) > maxSelectedRowIds) {
401
+ throw new PlatformApiError(`row_export: ${scope?.rowIds?.length} satır id'si, ${maxSelectedRowIds} sınırının üstünde. Bu kadar satırı seçerek değil filtreyle verin: scope { kind: 'view', filters: [...] }.`);
402
+ }
403
+ const resolvedFormat = format ?? 'xlsx';
404
+ const notes = [];
405
+ const resolver = new RefResolver(platform, project, environment);
406
+ const resolvedEntityId = await resolveEntityId(resolver, entityId);
407
+ const response = template
408
+ ? await platform.tenantWithHeaders('GET', project, `/entities/${resolvedEntityId}/import/template`, { environment, query: { format: resolvedFormat, locale }, expectBinary: true })
409
+ : await platform.tenantWithHeaders('POST', project, `/entities/${resolvedEntityId}/export`, {
410
+ environment,
411
+ expectBinary: true,
412
+ body: {
413
+ format: resolvedFormat,
414
+ scope: {
415
+ kind: scope?.kind ?? 'all',
416
+ ...(scope?.filters ? { filters: scope.filters } : {}),
417
+ ...(scope?.sort ? { sort: { field: scope.sort.field, ascending: scope.sort.ascending ?? true } } : {}),
418
+ ...(scope?.rowIds ? { rowIds: scope.rowIds } : {})
419
+ },
420
+ columns: columns ?? 'all',
421
+ ...(selectedFields ? { selectedFields } : {}),
422
+ relations: reimportable ? 'both' : relations ?? 'display',
423
+ languages: languages ?? 'request',
424
+ files: files ?? 'name',
425
+ reimportable: reimportable ?? false
426
+ }
427
+ });
428
+ const payload = response.body;
429
+ if (response.httpStatus === 202) {
430
+ const started = JSON.parse(payload.bytes.toString('utf8'));
431
+ return withResolved({
432
+ jobId: started.data?.jobId,
433
+ note: "Satır sayısı platformun eşiğini aştı, iş arka plana alındı. import_status { jobId } ile izleyin; tamamlanınca dosya result.reportFileId'dedir ve file_download ile okunur. Gateway beklemez."
434
+ }, resolver.resolved);
435
+ }
436
+ const noteParts = template ? [templateFormatNotes[resolvedFormat], 'İki satır sentetik örnektir — üzerine yazın ya da silin.'] : [];
437
+ let sample;
438
+ if (template && inline) {
439
+ const sampled = await platform.tenant('GET', project, `/entities/${resolvedEntityId}/import/sample`, { environment, query: { locale } });
440
+ const dto = sampled?.data ?? {};
441
+ const sampleJson = dto.json ?? '';
442
+ const sampleColumns = (dto.columns ?? []).map(column => ({
443
+ slug: column.slug,
444
+ header: column.header,
445
+ type: column.type,
446
+ required: column.required,
447
+ ...(column.options?.length ? { options: column.options } : {}),
448
+ ...(column.relationEntity ? { relationEntity: column.relationEntity } : {}),
449
+ ...(column.matchField && column.relationEntity ? { matchField: column.matchField } : {}),
450
+ ...(column.manual ? { manual: true } : {})
451
+ }));
452
+ const fits = sampleJson.length <= templateInlineMaxChars;
453
+ sample = {
454
+ entity: dto.entitySlug,
455
+ locale: dto.locale,
456
+ keyField: dto.keyField ?? null,
457
+ columns: sampleColumns,
458
+ ...(fits ? { data: JSON.parse(sampleJson || '{}').data ?? [] } : {})
459
+ };
460
+ if (!fits) {
461
+ noteParts.push(`Satır içi örnek ${sampleJson.length} karakter, ${templateInlineMaxChars} sınırının üstünde: sample.data atlandı, sample.columns duruyor. Örnek satırları görmek için şablon dosyasını file_download ile okuyun.`);
462
+ }
463
+ const maskedSlugs = sampleColumns.map(column => column.slug ?? '').filter(slug => secretKeyPattern.test(slug));
464
+ if (fits && maskedSlugs.length > 0) {
465
+ noteParts.push(`${maskedSlugs.join(', ')} örnek değeri gateway'in gizli alan maskesiyle *** döner — örnek değildir, bu sütuna kendi değerinizi yazın.`);
466
+ }
467
+ if (sampleColumns.some(column => column.manual)) {
468
+ noteParts.push("manual sütunlar boştur: ilişki hücresine hedef tablonun matchField değerini yazın (şablon hedeften satır okumaz).");
469
+ }
470
+ }
471
+ const fileName = exportFileName(response.headers, 'export', resolvedFormat);
472
+ const folderId = await exportFolderIdAsync(platform, project, environment);
473
+ const uploaded = await uploadFile(platform, {
474
+ project,
475
+ environment,
476
+ content: payload.bytes.toString('base64'),
477
+ filename: fileName,
478
+ contentType: payload.contentType,
479
+ folderId
480
+ });
481
+ const fileId = uploaded?.data?.id;
482
+ if (download) {
483
+ await writeFile(resolve(download), payload.bytes);
484
+ notes.push(`Dosya ayrıca ${resolve(download)} yoluna yazıldı.`);
485
+ }
486
+ if (reimportable) {
487
+ notes.push("Dosya id sütunu ve _zeyrek meta sayfası taşıyor: düzenledikten sonra file_upload → import_analyze → import_plan { mode: 'upsert', keyField: 'id' } ile geri yükleyin.");
488
+ }
489
+ if (resolvedFormat === 'csv' && template) {
490
+ notes.push(`CSV şablon UTF-8 BOM'lu; ayraç, ondalık ve tarih \`locale\` profilinden gelir (tr-TR \`;\` · \`1,5\` · \`dd.MM.yyyy\`, en-US \`,\` · \`1.5\` · \`M/d/yyyy\`)${locale ? `, seçilen ${locale}` : ", locale verilmediği için projenin varsayılan dili seçti"}. Geri yüklerken import_analyze ve import_plan'a aynı locale'i verin; plan analizin locale'ini devralmaz.`);
491
+ }
492
+ else if (resolvedFormat === 'csv') {
493
+ notes.push("CSV UTF-8 BOM'lu yazıldı; ayraç ve ondalık projenin dilinin profilinden gelir (tr → noktalı virgül `;` · `1,5`, diğer diller → virgül `,` · `1.5`). Dışa aktarmada locale seçilemez.");
494
+ }
495
+ if (locale && !template) {
496
+ notes.push('locale yalnız template ile kullanılır; dışa aktarmada yok sayıldı.');
497
+ }
498
+ return withNormalization(withResolved({
499
+ fileId,
500
+ fileName,
501
+ bytes: payload.bytes.byteLength,
502
+ contentType: payload.contentType,
503
+ folder: exportFolderName,
504
+ ...(sample ? { sample } : {}),
505
+ note: [`Dosya Sürücü'de. İçeriği okumak için file_download { id: '${String(fileId)}' }.`, ...noteParts].join(' ')
506
+ }, resolver.resolved), notes);
507
+ }));
188
508
  server.registerTool('row_delete', {
189
509
  title: 'Satır sil',
190
510
  description: "Tek satır için 'id', çoklu silme için 'ids' verin. Çok dilli entity'de dil satırlarını platform ana satırdan ÖNCE kendisi siler; eski platform sürümünde yabancı anahtar hatası dönerse önce companion entity'nin satırlarını silin.",
@@ -1,11 +1,15 @@
1
1
  import { invalidScreenEventNames, invalidWidgetTypeNodes, nestedRepeaterNodes, nestedRepeaterMessage, blockSizeNote, heightTypeNote, sizeStringNote } from '../knowledge/normalize.js';
2
+ import { FUNCTIONS_NAMESPACE } from '../knowledge/functionGuards.js';
2
3
  import { applyCatalogLayoutRules } from './components.js';
3
4
  import { PlatformApiError } from '../platform/errors.js';
4
5
  const GUARD_CODES = {
5
6
  page: { events: 'PAGE-011', widgets: 'PAGE-012', noun: 'sayfa', tree: 'schema' },
6
7
  layout: { events: 'LAY-004', widgets: 'LAY-005', noun: 'layout', tree: 'views' }
7
8
  };
8
- /** JS tanımlayıcısı olmayan `name` alanlarını toplar; ad `const <ad> = Components["<ad>"]` üretimine girer. */
9
+ /**
10
+ * JS tanımlayıcısı olmayan ya da AYRILMIŞ `name` alanlarını toplar; ad
11
+ * `const <ad> = Components["<ad>"]` üretimine girer.
12
+ */
9
13
  export function invalidComponentNames(nodes) {
10
14
  const found = [];
11
15
  for (const node of nodes ?? []) {
@@ -13,6 +17,11 @@ export function invalidComponentNames(nodes) {
13
17
  if (typeof name === 'string' && name.length > 0 && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name.replace(/-/g, ''))) {
14
18
  found.push(`'${name}' (${node.elementId ?? '?'})`);
15
19
  }
20
+ else if (typeof name === 'string' && name.replace(/-/g, '') === FUNCTIONS_NAMESPACE) {
21
+ // K1: preamble her komponent için `const <ad> = Components["<ad>"]` üretir; `Functions` adlı
22
+ // komponent kullanıcı fonksiyonlarının ad alanını ezer ve sayfadaki her Functions.x() ölür.
23
+ found.push(`'${name}' (${node.elementId ?? '?'}) — ayrılmış ad: ${FUNCTIONS_NAMESPACE} kullanıcı fonksiyonlarının ad alanıdır (K1)`);
24
+ }
16
25
  found.push(...invalidComponentNames(node.children));
17
26
  }
18
27
  return found;
@@ -37,7 +46,7 @@ export function assertScreenWriteGuards(kind, screen) {
37
46
  throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
38
47
  const badNames = invalidComponentNames(screen.schema);
39
48
  if (badNames.length > 0) {
40
- throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badNames.join(' · ')}. `
49
+ throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil ya da ayrılmış (istek gönderilmedi): ${badNames.join(' · ')}. `
41
50
  + 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; boşluklu ya da geçersiz bir ad '
42
51
  + 'SAYFADAKİ TÜM ifadeleri "Missing initializer in const declaration" ile öldürür (layout adları her sayfaya sızar). '
43
52
  + `name alanını elementId ile aynı yaz; okunabilir Türkçe başlık için props.title / props.text / props.label kullan (${codes.noun} ${codes.tree}).`, 400);
@@ -1,6 +1,30 @@
1
1
  import { guid, pagingParams, readOnly, writes, updates } from './common.js';
2
+ import { withNormalization } from '../knowledge/normalize.js';
2
3
  import { handle } from '../platform/responses.js';
3
4
  import { z } from 'zod';
5
+ /**
6
+ * `environmentId` verilmediğinde platform PRODUCTION'ı paketler
7
+ * (`CreateProjectAsTemplateCommand`: `EnvironmentId ??= ActiveEnvironments().First(f => f.IsProduction)`).
8
+ * Promote edilmemiş projede o environment boştur ve build yine 'success' döner; şablon seed
9
+ * kayıtlarından ibaret kalır. Çağıran bunu yanıttan görmediği için hangi environment'ın
10
+ * paketlendiğini adıyla söylüyoruz (2026-09-12 koşusu S-2).
11
+ */
12
+ async function defaultTemplateEnvironmentNote(platform, projectId) {
13
+ const fallback = "environmentId verilmedi: platform PRODUCTION environment'ını paketler (varsayılan/development değil). Promote edilmemiş projede şablon boş çıkar; development içeriğini istiyorsanız o environment'ın id'sini açıkça verin.";
14
+ try {
15
+ const payload = await platform.org('GET', `/projects/${projectId}`);
16
+ const environments = Array.isArray(payload?.data?.environments)
17
+ ? payload.data.environments
18
+ : [];
19
+ const production = environments.find(environment => environment.isProduction === true);
20
+ if (!production)
21
+ return fallback;
22
+ return `environmentId verilmedi: platform '${String(production.name)}' (production, id ${String(production.id)}) environment'ını paketler — varsayılan/development environment'ı DEĞİL. Oraya henüz promote etmediyseniz şablon yalnız seed kayıtlarını taşır; development içeriğini paketlemek için o environment'ın id'sini açıkça verin.`;
23
+ }
24
+ catch {
25
+ return fallback;
26
+ }
27
+ }
4
28
  export function registerTemplateTools(server, platform) {
5
29
  const config = platform.config;
6
30
  server.registerTool('template_list', {
@@ -24,31 +48,34 @@ export function registerTemplateTools(server, platform) {
24
48
  description: "Şablonun bir modüldeki kayıtlarını listeler (project_create'in templateOptions.filters'ını doldurmak için).",
25
49
  inputSchema: {
26
50
  id: guid,
27
- module: z.string().describe("Module değeri: entity | microservice | workflow | page | layout | component | query | view …")
51
+ module: z.string().describe("Module değeri: entity | microservice | workflow | page | layout | component | query | view | variable | global-function (fonksiyonlar, iki kaynak) | flow … (platform Module enum değeri, kebab-case)")
28
52
  },
29
53
  annotations: readOnly
30
54
  }, handle(config, ({ id, module }) => platform.org('GET', `/templates/${id}/items`, { query: { module } })));
31
55
  server.registerTool('template_create', {
32
56
  title: 'Projeyi şablon olarak yayımla',
33
- description: 'Mevcut projeden yeni bir şablon sürümü üretir. Görsel yükleme bu tool\'da desteklenmez (ikili aktarım kapsam dışı).',
57
+ description: "Mevcut projeden yeni bir şablon sürümü üretir. **Paketlenen environment `environmentId`'dir ve boş bırakılırsa platform PRODUCTION'ı paketler** (`CreateProjectAsTemplateCommand`: `EnvironmentId ??= ActiveEnvironments().First(f => f.IsProduction)`) — 'varsayılan environment' DEĞİL. Henüz promote edilmemiş projede production boştur: build 'success' döner ama şablon yalnız seed kayıtlarını taşır (microfunction/workflow/microservice sıfır). Geliştirmede yazdığınız içeriği paketlemek için development environment id'sini AÇIKÇA verin. Build asenkrondur: yanıt hemen döner, sürüm birkaç saniye sonra template_list'te görünür; içeriği template_items ile doğrulayın. Görsel yükleme bu tool'da desteklenmez (ikili aktarım kapsam dışı).",
34
58
  inputSchema: {
35
59
  projectId: guid,
36
60
  name: z.string().min(1),
37
61
  version: z.string().min(1).describe("Sürüm etiketi, ör. '1.0.0'"),
38
62
  description: z.string().optional(),
39
63
  changeLog: z.string().optional(),
40
- environmentId: guid.optional().describe('Hangi environment\'ın içeriği paketlenecek; boşsa varsayılan')
64
+ environmentId: guid.optional().describe("Hangi environment'ın içeriği paketlenecek. BOŞSA PRODUCTION paketlenir (projenin varsayılan/development environment'ı değil) — development içeriğini şablona koyacaksanız o environment'ın id'sini açıkça verin.")
41
65
  },
42
66
  annotations: writes
43
- }, handle(config, ({ projectId, name, version, description, changeLog, environmentId }) => {
67
+ }, handle(config, async ({ projectId, name, version, description, changeLog, environmentId }) => {
44
68
  const form = new URLSearchParams({ Name: name, Version: version });
69
+ const normalized = [];
45
70
  if (description)
46
71
  form.set('Description', description);
47
72
  if (changeLog)
48
73
  form.set('ChangeLog', changeLog);
49
74
  if (environmentId)
50
75
  form.set('EnvironmentId', environmentId);
51
- return platform.org('POST', `/projects/${projectId}/templates`, { form });
76
+ else
77
+ normalized.push(await defaultTemplateEnvironmentNote(platform, projectId));
78
+ return withNormalization(await platform.org('POST', `/projects/${projectId}/templates`, { form }), normalized);
52
79
  }));
53
80
  server.registerTool('template_check_updates', {
54
81
  title: 'Şablon güncellemesi var mı',
@@ -4,7 +4,7 @@ import { evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/poli
4
4
  import { workflowOutline, resolvePart, resolvePatchOperations } from '../knowledge/outlines.js';
5
5
  import { patchResult, baseVersionOf } from '../platform/patch.js';
6
6
  import { writeDiff, withWriteDiff } from '../platform/writeDiff.js';
7
- import { policyRules } from '../knowledge/rules.js';
7
+ import { policyRules, microfunctionStepFindings } from '../knowledge/rules.js';
8
8
  import { languageHeader } from '../knowledge/languageCodes.js';
9
9
  import { handle } from '../platform/responses.js';
10
10
  import fastJsonPatch from 'fast-json-patch';
@@ -116,7 +116,10 @@ export function registerWorkflowTools(server, platform) {
116
116
  annotations: writes
117
117
  }, handle(config, async ({ project, environment, applicationId, definition, policyOverride }) => {
118
118
  const normalized = normalizeWorkflowDefinition(definition);
119
- const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'workflow', definition, { operation: 'create', workflowKind: definition.type }, policyOverride));
119
+ const findings = enforcePolicy([
120
+ ...evaluatePolicy(config, policyRules, 'workflow', definition, { operation: 'create', workflowKind: definition.type }, policyOverride),
121
+ ...await microfunctionStepFindings(platform, project, environment, definition, definition.type, policyOverride)
122
+ ]);
120
123
  const drift = unmappedMembers('CreateWorkflowCommand', definition);
121
124
  const payload = await platform.tenant('POST', project, `/workflows/${applicationId}`, { environment, body: definition });
122
125
  return attachFindings(withUnmappedMembers(withWriteDiff(withNormalization(payload, normalized), writeDiff(definition, payload)), drift), findings);
@@ -128,7 +131,10 @@ export function registerWorkflowTools(server, platform) {
128
131
  annotations: updates
129
132
  }, handle(config, async ({ project, environment, applicationId, id, definition, policyOverride }) => {
130
133
  const normalized = normalizeWorkflowDefinition(definition);
131
- const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'workflow', definition, { operation: 'update', workflowKind: definition.type }, policyOverride));
134
+ const findings = enforcePolicy([
135
+ ...evaluatePolicy(config, policyRules, 'workflow', definition, { operation: 'update', workflowKind: definition.type }, policyOverride),
136
+ ...await microfunctionStepFindings(platform, project, environment, definition, definition.type, policyOverride)
137
+ ]);
132
138
  const drift = unmappedMembers('UpdateWorkflowCommand', definition);
133
139
  const payload = await platform.tenant('PUT', project, `/workflows/${applicationId}/${id}`, { environment, body: definition });
134
140
  return attachFindings(withUnmappedMembers(withWriteDiff(withNormalization(payload, normalized), writeDiff(definition, payload)), drift), findings);
@@ -145,7 +151,10 @@ export function registerWorkflowTools(server, platform) {
145
151
  const resolved = resolvePatchOperations(definition, operations);
146
152
  const patched = fastJsonPatch.applyPatch(definition, resolved.operations, true, false).newDocument;
147
153
  const normalized = normalizeWorkflowDefinition(patched);
148
- const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'workflow', patched, { operation: 'update', workflowKind: patched.type }, policyOverride));
154
+ const findings = enforcePolicy([
155
+ ...evaluatePolicy(config, policyRules, 'workflow', patched, { operation: 'update', workflowKind: patched.type }, policyOverride),
156
+ ...await microfunctionStepFindings(platform, project, environment, patched, patched.type, policyOverride)
157
+ ]);
149
158
  const drift = introducedUnmappedMembers('UpdateWorkflowCommand', (current?.data ?? current), patched);
150
159
  // Gövde değil FARK gönderilir: gateway normalizasyonları da op'a çevrilsin diye net diff
151
160
  // okunan kayıt ile son hâl arasından hesaplanır (tam PUT yok, yalnız değişen alanlar yazılır).
@@ -176,12 +185,21 @@ export function registerWorkflowTools(server, platform) {
176
185
  }, handle(config, ({ project, environment, id }) => platform.tenant('POST', project, `/workflows/clone/${id}`, { environment })));
177
186
  server.registerTool('workflow_preview', {
178
187
  title: 'Workflow önizle',
179
- description: "Kaydedilmemiş workflow tanımını çalıştırır; kaydetmeden önce her akışı preview ile doğrulayın. definition = PreviewWorkflowCommand gövdesi. Test girdisi 'previewPayload' anahtarıyla verilir ('parameters'/'input' ÇALIŞMAZ), değişkenler 'previewVariables'. previewPayload STRING olmalıdır (JSON.stringify(...) ya da \"({ id: 1 })\") — nesne gönderilirse platform 'Unexpected end of input' ile 500 döner. Preview tek transaction'da koşar ve sonunda ROLLBACK edilir: table create/update step'leri kalıcı satır YAZMAZ (ölçüldü, İK koşusu W-1). Tek istisna code step içindeki dış HTTP çağrılarıdır — onlar geri alınamaz. Davranış kuralları: platform_guide('workflow'). " + constraintSummary('PreviewWorkflowCommand'),
188
+ description: "Kaydedilmemiş workflow tanımını çalıştırır; kaydetmeden önce her akışı preview ile doğrulayın. definition = PreviewWorkflowCommand gövdesi. Test girdisi 'previewPayload' anahtarıyla verilir ('parameters'/'input' ÇALIŞMAZ), değişkenler 'previewVariables'. previewPayload STRING olmalıdır (JSON.stringify(...) ya da \"({ id: 1 })\") — nesne gönderilirse platform 'Unexpected end of input' ile 500 döner. Akışın parameterDtoId'si YOKSA previewPayload verilmez (platform 400 döner; gateway alanı düşürüp gatewayNormalized ile bildirir). Preview tek transaction'da koşar ve sonunda ROLLBACK edilir: table create/update step'leri kalıcı satır YAZMAZ (ölçüldü, İK koşusu W-1). Tek istisna code step içindeki dış HTTP çağrılarıdır — onlar geri alınamaz. Davranış kuralları: platform_guide('workflow'). " + constraintSummary('PreviewWorkflowCommand'),
180
189
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, definition: definitionParam, policyOverride: policyOverrideParam, language: languageParam },
181
190
  annotations: writes
182
191
  }, handle(config, async ({ project, environment, applicationId, definition, policyOverride, language }) => {
183
192
  const normalized = normalizeWorkflowDefinition(definition);
184
- const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'workflow', definition, { operation: 'preview', workflowKind: definition.type }, policyOverride));
193
+ // 33/S-6: parameterDtoId olmayan akışa previewPayload gönderilince platform 400 verir
194
+ // ("'previewPayload' requires a parameter DTO"). Girdisiz akışta alanın anlamı yok — düşürülür.
195
+ if (!definition.parameterDtoId && definition.previewPayload !== undefined) {
196
+ delete definition.previewPayload;
197
+ normalized.push("previewPayload düşürüldü — akışın parameterDtoId'si yok, platform 400 \"'previewPayload' requires a parameter DTO\" verir; girdi gerekiyorsa önce parameterDtoId + parameterBehavior tanımlayın");
198
+ }
199
+ const findings = enforcePolicy([
200
+ ...evaluatePolicy(config, policyRules, 'workflow', definition, { operation: 'preview', workflowKind: definition.type }, policyOverride),
201
+ ...await microfunctionStepFindings(platform, project, environment, definition, definition.type, policyOverride)
202
+ ]);
185
203
  const payload = await platform.tenant('POST', project, `/workflows/${applicationId}/preview`, { environment, body: definition, headers: languageHeader(language, normalized) });
186
204
  return attachFindings(withNormalization(payload, normalized), findings);
187
205
  }));