@appaflytech/zeyrek-ai-mcp 0.0.20 → 0.0.22

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.
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import { PlatformApiError } from './errors.js';
3
+ /**
4
+ * `ifExists` parametresi. Aynı ad ya da slug'la ikinci kaydın `"<ad> (1)"` olarak açılması platformun
5
+ * ürün kararıdır (akış, sayfa, workflow ve microservice servisleri aynı sayaç kuralını uygular) ve
6
+ * değişmez. MCP ajanı çağrıyı tekrar edebildiği için varsayılan "yazma, mevcut id'yi söyle"dir.
7
+ */
8
+ export function ifExistsParam(noun, updateNote) {
9
+ return z.enum(['error', 'update', 'duplicate']).default('error').describe(`Aynı ad ya da slug'da (büyük/küçük harf duyarsız) ${noun} varsa ne olacağı. 'error' (varsayılan): yazmaz, mevcut kaydın id'sini döner. 'update': ${updateNote}. 'duplicate': platformun ürün davranışı — ikinci kayıt açılır, platform ada/slug'a ' (1)' sayacını ekler (Studio'daki 'New … (1)' ile aynı kural).`);
10
+ }
11
+ const upper = (value) => String(value ?? '').toUpperCase();
12
+ const isPresent = (value) => value !== undefined && value !== null && String(value) !== '';
13
+ /**
14
+ * Var-mı kapısı (FLOW-026 ve kardeşleri). Aday araması platformda harfe duyarlıdır; gateway bilinçli
15
+ * olarak harf duyarsız karşılaştırır. Var-mı okuması başarısız olursa çağrı düşer — okuyamadığı
16
+ * listeyi boş sayan kontrol tam da zaman aşımında kopya üretirdi.
17
+ */
18
+ export async function existingMatch(options) {
19
+ const { code, noun, where, name, slug, id, idParam, ifExists } = options;
20
+ const slugCounter = (options.slugSuffix ?? 'dash') === 'dash' ? "'-1'" : "' (1)'";
21
+ if (ifExists === 'duplicate')
22
+ return undefined;
23
+ if (!name && !slug)
24
+ return undefined;
25
+ let records;
26
+ try {
27
+ records = await options.list();
28
+ }
29
+ catch (error) {
30
+ throw new PlatformApiError(`${code}: mevcut ${noun} kayıtları okunamadı, ${noun} YAZILMADI (${error instanceof Error ? error.message : String(error)}). Liste okunmadan yazmak aynı adla ikinci kayıt açabilir; ${options.listTool} ile tekrar deneyin ya da bilerek ikinci kayıt istiyorsanız ifExists:'duplicate' verin.`);
31
+ }
32
+ const matches = records.filter(record => (name !== undefined && name !== '' && upper(record.name) === upper(name)) ||
33
+ (slug !== undefined && slug !== '' && upper(record.slug) === upper(slug)));
34
+ const [match] = matches;
35
+ if (match === undefined)
36
+ return undefined;
37
+ const describe = (record) => `${String(record.id)} ('${String(record.name)}', slug '${String(record.slug)}')`;
38
+ const copyPattern = name
39
+ ? new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} \\(\\d+\\)$`, 'i')
40
+ : undefined;
41
+ const copies = copyPattern ? records.filter(record => copyPattern.test(String(record.name ?? ''))) : [];
42
+ const copyNote = copies.length > 0
43
+ ? ` ${where[0].toUpperCase()}${where.slice(1)} bu adın sayaçlı eşleri de var: ${copies.map(describe).join(' · ')} — ${options.copyCheck}.`
44
+ : '';
45
+ if (ifExists === 'error') {
46
+ throw new PlatformApiError(`${code}: ${where} aynı ad ya da slug'da ${noun} zaten var: ${matches.map(describe).join(' · ')}. İstek gönderilmedi — gönderilseydi platform ürün kuralı gereği ikinci bir ${noun} açardı (çakışan ada ' (1)', slug'a ${slugCounter} sayacı eklenerek; yalnız harf farkı varsa sayaçsız). Mevcut kaydı güncellemek için aynı çağrıyı ifExists:'update' ile tekrarlayın; bilerek ikinci kayıt istiyorsanız ifExists:'duplicate'.${copyNote}`, 409);
47
+ }
48
+ if (matches.length > 1) {
49
+ throw new PlatformApiError(`${code}: ad ve slug farklı kayıtlara çıkıyor (${matches.map(describe).join(' · ')}); hangisinin güncelleneceği belirsiz, istek gönderilmedi. ${options.updateTool} kullanın.${copyNote}`, 409);
50
+ }
51
+ // Eşleşme tipten bağımsızdır (platform sayacı tipe bakmaz), ama 'update' tipi farklı kaydın üstüne
52
+ // yazarsa kaydın türü değişir: backend PUT'ta tipi de yazar ve middleware automation'a dönüşür (R-1).
53
+ const kind = options.kind;
54
+ if (kind !== undefined && isPresent(kind.value) && isPresent(match[kind.key]) && upper(match[kind.key]) !== upper(kind.value)) {
55
+ throw new PlatformApiError(`${code}: aynı ad ya da slug'daki mevcut kayıt ${describe(match)} ${kind.key}='${String(match[kind.key])}'; gönderilen gövde ${kind.key}='${String(kind.value)}'. ifExists:'update' kaydın türünü değiştirirdi — istek gönderilmedi. Farklı ad/slug seçin ya da bilerek ikinci kayıt istiyorsanız ifExists:'duplicate'.${copyNote}`, 409);
56
+ }
57
+ if (id !== undefined && id.toLowerCase() !== String(match.id).toLowerCase()) {
58
+ throw new PlatformApiError(`${code}: verilen ${idParam} (${id}) aynı adlı mevcut kayıtla (${describe(match)}) çelişiyor; istek gönderilmedi. Mevcut kaydı güncellemek için ${idParam}'yi vermeyin ya da ${String(match.id)} verin.`, 409);
59
+ }
60
+ return match;
61
+ }
62
+ const LIST_PAGE_LENGTH = 200;
63
+ const LIST_PAGE_LIMIT = 50;
64
+ /**
65
+ * Uygulama düzeyindeki liste uçlarını (`GET /microservices|workflows|pages/{app}`) sonuna kadar okur.
66
+ * Arama terimi kullanılmaz: platformun `searchTerm`'ü slug'ı kapsamayabilir ve eksik liste kopya açar.
67
+ *
68
+ * Platform sayfaları kararlı sırada vermiyor (R-3): bir kayıt iki sayfada gelip başka biri hiç
69
+ * gelmeyebilir. İlk yanıttaki `meta.totalData` okunan TEKİL id sayısıyla tutmazsa liste tutarsızdır ve
70
+ * hata atılır — çağıran yazmaz. `meta` taşımayan eski yanıtta denetim yapılamaz.
71
+ */
72
+ export async function listAll(platform, project, environment, path) {
73
+ const records = [];
74
+ let expected;
75
+ for (let pageIndex = 0; pageIndex < LIST_PAGE_LIMIT; pageIndex++) {
76
+ const payload = await platform.tenant('GET', project, path, { environment, query: { pageIndex, pageLength: LIST_PAGE_LENGTH } });
77
+ if (!Array.isArray(payload?.data))
78
+ throw new PlatformApiError('liste yanıtında data dizisi yok');
79
+ if (pageIndex === 0 && typeof payload.meta?.totalData === 'number')
80
+ expected = payload.meta.totalData;
81
+ records.push(...payload.data);
82
+ if (payload.data.length < LIST_PAGE_LENGTH || (expected !== undefined && records.length >= expected)) {
83
+ const unique = new Set(records.map(record => String(record.id).toLowerCase())).size;
84
+ if (expected !== undefined && unique !== expected) {
85
+ throw new PlatformApiError(`liste tutarsız okundu: platform ${expected} kayıt dedi, sayfalardan ${unique} tekil kayıt geldi (${records.length - unique} tekrar) — sayfa sırası kararlı değil`);
86
+ }
87
+ return records;
88
+ }
89
+ }
90
+ throw new PlatformApiError(`liste ${LIST_PAGE_LENGTH * LIST_PAGE_LIMIT} kaydı aştı, tamamı okunamadı`);
91
+ }
92
+ /**
93
+ * `ifExists:'duplicate'` yazmasında platform ada ' (n)', slug'a '-n' (akışta ' (n)') ekler. Bu fark kayıp
94
+ * değildir (Gözlem 1): kalıba uyan alan karşılaştırmadan ÖNCE gönderilen gövdede kaydedilen değerle
95
+ * değiştirilir ve `renamedByPlatform`'a gider. Kalıba uymayan fark gövdede kalır, yani yine kayıptır.
96
+ */
97
+ export function platformRename(sent, saved, slugSuffix) {
98
+ const record = saved && typeof saved === 'object' && !Array.isArray(saved) ? saved : {};
99
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
100
+ const renamed = {};
101
+ const adjusted = { ...sent };
102
+ for (const key of ['name', 'slug']) {
103
+ const given = sent[key];
104
+ const stored = record[key];
105
+ if (typeof given !== 'string' || typeof stored !== 'string' || given === stored)
106
+ continue;
107
+ const pattern = key === 'slug' && slugSuffix === 'dash'
108
+ ? new RegExp(`^${escape(given)}-\\d+$`)
109
+ : new RegExp(`^${escape(given)} \\(\\d+\\)$`);
110
+ if (!pattern.test(stored))
111
+ continue;
112
+ renamed[key] = stored;
113
+ adjusted[key] = stored;
114
+ }
115
+ return Object.keys(renamed).length === 0 ? { sent: adjusted } : { sent: adjusted, renamed };
116
+ }
117
+ export function withRename(payload, renamed) {
118
+ if (renamed === undefined || !payload || typeof payload !== 'object' || Array.isArray(payload))
119
+ return payload;
120
+ return { ...payload, renamedByPlatform: renamed };
121
+ }
122
+ /**
123
+ * Yazma yanıtına `outcome` ekler; ajan kaydın yeni mi açıldığını yoksa güncellendiğini mi tek alandan okur.
124
+ */
125
+ export function withOutcome(payload, existing) {
126
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
127
+ return payload;
128
+ return { ...payload, outcome: existing === undefined ? 'created' : 'updated' };
129
+ }
@@ -1,3 +1,4 @@
1
+ import { compactDiagnostics } from './diagnostics.js';
1
2
  import { logError, logStep } from '../observability/trace.js';
2
3
  import { PlatformApiError, describeErrorBody } from './errors.js';
3
4
  import { PolicyBlockError } from '../knowledge/policy.js';
@@ -139,7 +140,7 @@ function asStructured(payload) {
139
140
  return { data: payload };
140
141
  }
141
142
  export function toolResult(payload, config, expectFailure) {
142
- const pruned = prune(payload);
143
+ const pruned = prune(compactDiagnostics(payload));
143
144
  const { payload: safe, masked, tokenRefs } = maskSecrets(pruned);
144
145
  const runtimeErrors = runtimeErrorSummary(safe);
145
146
  if (runtimeErrors.length > 0) {
@@ -1,4 +1,15 @@
1
1
  import { z } from 'zod';
2
+ import { diagnosticsParam, runWithDiagnostics } from './diagnostics.js';
3
+ /**
4
+ * `writes`/`updates` açıklamalı ama KAYIT yazmayan tool'lar: çalıştırma, önizleme, dışa aktarma, indirme,
5
+ * bağlantı testi. Yanıtlarında yazma tanılaması (silentlyDropped) yoktur; parametre yalnız tool listesini
6
+ * büyütür. Yeni böyle bir tool eklenirse buraya yazılır — `test/diagnostics.test.ts` listeyi kilitler.
7
+ */
8
+ const NON_WRITING_TOOLS = new Set([
9
+ 'microservice_execute', 'microservice_preview', 'microservice_debug', 'query_preview', 'workflow_preview',
10
+ 'automation_preview', 'automation_trigger', 'row_export', 'file_download', 'microfunction_invoke',
11
+ 'microfunction_runtime_test_connection'
12
+ ]);
2
13
  function isRawShape(value) {
3
14
  if (!value || typeof value !== 'object' || '_def' in value)
4
15
  return false;
@@ -19,14 +30,23 @@ export function rejectUnknownArguments(server) {
19
30
  server.registerTool = ((name, config, callback) => {
20
31
  if (!isRawShape(config.inputSchema))
21
32
  return registerTool(name, config, callback);
22
- const shape = config.inputSchema;
33
+ // Yazma tool'ları tanılama kipini alır; parametre handler'a ULAŞMAZ (gövdeyi `...rest` ile
34
+ // platforma geçiren tool'larda sızardı), yalnız toolResult'ın okuduğu bağlama konur.
35
+ const writesDiagnostics = config.annotations?.readOnlyHint === false && config.annotations.destructiveHint !== true && !NON_WRITING_TOOLS.has(name);
36
+ const shape = writesDiagnostics ? { ...config.inputSchema, diagnostics: diagnosticsParam } : config.inputSchema;
37
+ const run = writesDiagnostics
38
+ ? ((args, extra) => {
39
+ const { diagnostics, ...rest } = args ?? {};
40
+ return runWithDiagnostics(diagnostics === 'full' ? 'full' : 'compact', () => callback(rest, extra));
41
+ })
42
+ : callback;
23
43
  const accepted = Object.keys(shape).join(', ');
24
44
  const inputSchema = z.object(shape, {
25
45
  errorMap: (issue, context) => issue.code === 'unrecognized_keys'
26
46
  ? { message: `${name} şu üst düzey argümanları tanımıyor: ${issue.keys.map(key => `'${key}'`).join(', ')}. Tanınmayan alan sessizce yok sayılmaz — çağrı hiç gönderilmedi. Bu tool'un okuduğu alanlar: ${accepted}.` }
27
47
  : { message: context.defaultError }
28
48
  }).strict();
29
- return registerTool(name, { ...config, inputSchema }, callback);
49
+ return registerTool(name, { ...config, inputSchema }, run);
30
50
  });
31
51
  return server;
32
52
  }
@@ -1,15 +1,17 @@
1
1
  import { PROP_VALUE_TYPES } from '../knowledge/normalize.js';
2
2
  import { logStep } from '../observability/trace.js';
3
3
  const maxDrops = 20;
4
+ const maxCollected = 1000;
4
5
  const maxDepth = 12;
5
6
  const missing = Symbol('missing');
6
7
  /**
7
8
  * Platformun kendi doldurduğu alanlar. Yalnız gönderilen tarafta da varsalar (patch yolunda gövde
8
9
  * okunup geri yazılır) fark üretirlerdi ve bu fark hep gürültüdür: `audit.updatedAt` her yazmada
9
- * değişir. 09-05 koşusunda `microservice_patch` yanıtındaki iki satırdan biri buydu.
10
+ * değişir. 09-05 koşusunda `microservice_patch` yanıtındaki iki satırdan biri buydu. `version` iyimser
11
+ * eşzamanlılık sayacıdır: okunan belge geri yazılınca platform onu artırır, fark her zaman gürültüdür.
10
12
  */
11
13
  const serverManagedKeys = new Set([
12
- 'audit', 'createdAt', 'updatedAt', 'createdUserId', 'createdUserName', 'updatedUserId', 'updatedUserName'
14
+ 'audit', 'createdAt', 'updatedAt', 'createdUserId', 'createdUserName', 'updatedUserId', 'updatedUserName', 'version'
13
15
  ]);
14
16
  /**
15
17
  * Platformun KENDİ ürettiği kimlik. Gönderilen id kaydedilenden farklıysa bu bir kayıp değildir:
@@ -50,6 +52,29 @@ export const unstoredKeys = new Set(['fieldSlug']);
50
52
  const canonicalValues = {
51
53
  type: [['string', 'text']]
52
54
  };
55
+ export function emptyWriteDiff() {
56
+ return { dropped: [], normalized: [], droppedCount: 0, droppedCountCapped: false, notes: [] };
57
+ }
58
+ function addDrop(sink, line) {
59
+ if (sink.droppedCount >= maxCollected) {
60
+ sink.droppedCountCapped = true;
61
+ return;
62
+ }
63
+ sink.dropped.push(line);
64
+ sink.droppedCount++;
65
+ }
66
+ /** Listeyi `maxDrops` satırda keser; sayı ve tavan bilgisi notlara yazılır. */
67
+ function finishReport(sink) {
68
+ if (sink.dropped.length > maxDrops) {
69
+ const total = sink.droppedCountCapped ? `en az ${sink.droppedCount}` : `toplam ${sink.droppedCount}`;
70
+ sink.dropped = sink.dropped.slice(0, maxDrops);
71
+ sink.notes.push(`ilk ${maxDrops} fark listelendi, ${total} — gövdeyi *_get ile okuyup karşılaştırın.`);
72
+ }
73
+ else if (sink.droppedCountCapped) {
74
+ sink.notes.push(`sayım en az ${sink.droppedCount} farkta durdu.`);
75
+ }
76
+ return sink;
77
+ }
53
78
  /**
54
79
  * Yolun son alan adı; `tags[0]` ya da `fields[fieldId=x]` gibi indeks/anahtar ekini atar.
55
80
  */
@@ -81,11 +106,11 @@ function lookup(saved, key) {
81
106
  return Object.hasOwn(record, key) ? record[key] : missing;
82
107
  }
83
108
  function compare(sent, saved, path, sink, depth) {
84
- if (sink.dropped.length >= maxDrops || depth > maxDepth)
109
+ if (sink.droppedCountCapped || depth > maxDepth)
85
110
  return;
86
111
  if (Array.isArray(sent)) {
87
112
  if (saved === missing) {
88
- sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
113
+ addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
89
114
  return;
90
115
  }
91
116
  const key = Array.isArray(saved) ? identityKeyFor(sent) : undefined;
@@ -103,18 +128,18 @@ function compare(sent, saved, path, sink, depth) {
103
128
  }
104
129
  return;
105
130
  }
106
- const before = sink.dropped.length;
131
+ const before = sink.droppedCount;
107
132
  sent.forEach((entry, index) => compare(entry, lookup(saved, index), `${path}[${index}]`, sink, depth + 1));
108
133
  // Sıra tabanlı karşılaştırmada "değişmiş" görünen öğe aslında yer değiştirmiş olabilir. Tek
109
134
  // elemanlı dizide sıra farkı imkânsız olduğu için not düşülmez (2026-09-06 · S-1).
110
- if (Array.isArray(saved) && sent.length > 1 && saved.length > 1 && sink.dropped.length > before) {
111
- sink.dropped.splice(before, 0, `${path}: (sıra tabanlı karşılaştırma — anahtar yok) aşağıdaki farklar platform diziyi başka sırada sakladıysa yanıltıcıdır`);
135
+ if (Array.isArray(saved) && sent.length > 1 && saved.length > 1 && sink.droppedCount > before) {
136
+ sink.notes.push(`${path}: (sıra tabanlı karşılaştırma — anahtar yok) aşağıdaki farklar platform diziyi başka sırada sakladıysa yanıltıcıdır`);
112
137
  }
113
138
  return;
114
139
  }
115
140
  if (sent && typeof sent === 'object') {
116
141
  if (saved === missing) {
117
- sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
142
+ addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
118
143
  return;
119
144
  }
120
145
  for (const [key, entry] of Object.entries(sent)) {
@@ -130,7 +155,7 @@ function compare(sent, saved, path, sink, depth) {
130
155
  if (unstoredKeys.has(field) && (saved === missing || saved === null))
131
156
  return;
132
157
  if (saved === missing) {
133
- sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
158
+ addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
134
159
  return;
135
160
  }
136
161
  if (sent === saved)
@@ -141,7 +166,7 @@ function compare(sent, saved, path, sink, depth) {
141
166
  sink.normalized.push(`${path}: gönderilen ${render(sent)} platformun kanonik adıyla ${render(saved)} olarak saklandı — eşdeğer, kayıp değil`);
142
167
  return;
143
168
  }
144
- sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen ${render(saved)}`);
169
+ addDrop(sink, `${path}: gönderilen ${render(sent)} ↔ kaydedilen ${render(saved)}`);
145
170
  }
146
171
  /**
147
172
  * Sol taraf platforma GİDEN gövdedir (gateway normalizasyonu uygulandıktan sonrası), sağ taraf
@@ -152,8 +177,8 @@ function compare(sent, saved, path, sink, depth) {
152
177
  * İki liste döner: `dropped` gerçekten kaybolan alanlar, `normalized` platformun kanonik adıyla
153
178
  * sakladığı eşdeğerler. İkincisi `gatewayNormalized`a gider, uyarıya değil.
154
179
  */
155
- export function writeDiff(sent, saved, aliases = {}) {
156
- const sink = { dropped: [], normalized: [] };
180
+ export function writeDiff(sent, saved, aliases = {}, finish = true) {
181
+ const sink = emptyWriteDiff();
157
182
  if (!sent || typeof sent !== 'object')
158
183
  return sink;
159
184
  const wrapper = saved && typeof saved === 'object' && !Array.isArray(saved)
@@ -171,19 +196,16 @@ export function writeDiff(sent, saved, aliases = {}) {
171
196
  if (keys.length > 0 && shared.length === 0)
172
197
  return sink;
173
198
  compare(comparable, root, '', sink, 0);
174
- if (sink.dropped.length >= maxDrops) {
175
- sink.dropped.push(`… ilk ${maxDrops} fark listelendi, daha fazla fark var — gövdeyi *_get ile okuyup karşılaştırın.`);
176
- }
177
- return sink;
199
+ return finish ? finishReport(sink) : sink;
178
200
  }
179
201
  export function silentlyDropped(sent, saved, aliases = {}) {
180
202
  return writeDiff(sent, saved, aliases).dropped;
181
203
  }
182
204
  export function withWriteDiff(payload, diff) {
183
- const drops = Array.isArray(diff) ? diff : diff.dropped;
184
- const normalized = Array.isArray(diff) ? [] : diff.normalized;
205
+ const drops = diff.dropped;
206
+ const normalized = diff.normalized ?? [];
185
207
  if (drops.length > 0) {
186
- logStep('sessiz düşme ⚠', `${drops.length} alan gönderildi ama kayda geçmedi — platform hata vermeden yok saydı, yanıta silentlyDropped olarak eklenir`, drops);
208
+ logStep('sessiz düşme ⚠', `${diff.droppedCount}${diff.droppedCountCapped ? '+' : ''} alan gönderildi ama kayda geçmedi — platform hata vermeden yok saydı, yanıta silentlyDropped olarak eklenir`, [...drops, ...diff.notes]);
187
209
  }
188
210
  if (normalized.length > 0) {
189
211
  logStep('düzeltme', `platform ${normalized.length} alanı kanonik adıyla sakladı — eşdeğer, yanıta gatewayNormalized olarak eklenir`, normalized);
@@ -195,7 +217,9 @@ export function withWriteDiff(payload, diff) {
195
217
  const existing = payload.gatewayNormalized;
196
218
  return {
197
219
  ...payload,
198
- ...(drops.length > 0 ? { silentlyDropped: drops } : {}),
220
+ ...(drops.length > 0 ? { silentlyDropped: drops, droppedCount: diff.droppedCount } : {}),
221
+ ...(drops.length > 0 && diff.droppedCountCapped ? { droppedCountCapped: true } : {}),
222
+ ...(drops.length > 0 && diff.notes.length > 0 ? { silentlyDroppedNotes: diff.notes } : {}),
199
223
  ...(normalized.length > 0
200
224
  ? { gatewayNormalized: [...(Array.isArray(existing) ? existing : []), ...normalized] }
201
225
  : {})
@@ -247,9 +271,9 @@ function findByElementId(nodes, elementId) {
247
271
  * yanıt yalnız `appliedPatch` olduğu için karşılaştırma yamadan SONRA okunan şemaya dayanır.
248
272
  */
249
273
  export function schemaOpDrops(schema, ops) {
274
+ const drops = { dropped: [], droppedCount: 0, droppedCountCapped: false, notes: [] };
250
275
  if (!Array.isArray(ops))
251
- return [];
252
- const drops = [];
276
+ return drops;
253
277
  for (const op of ops) {
254
278
  if (op.op === 'add_component') {
255
279
  const component = (op.component ?? {});
@@ -258,12 +282,10 @@ export function schemaOpDrops(schema, ops) {
258
282
  if (!elementId)
259
283
  continue;
260
284
  if (!saved) {
261
- drops.push(`add_component '${elementId}': komponent şemada bulunamadı — yama 200 döndü ama düğüm kaydedilmemiş.`);
285
+ addDrop(drops, `add_component '${elementId}': komponent şemada bulunamadı — yama 200 döndü ama düğüm kaydedilmemiş.`);
262
286
  continue;
263
287
  }
264
- for (const entry of silentlyDropped(unwrapProps(component), unwrapProps(saved))) {
265
- drops.push(`add_component '${elementId}' → ${entry}`);
266
- }
288
+ mergeNested(drops, writeDiff(unwrapProps(component), unwrapProps(saved), {}, false), `add_component '${elementId}' → `);
267
289
  continue;
268
290
  }
269
291
  if (op.op === 'replace_prop' && typeof op.target === 'string' && typeof op.prop === 'string') {
@@ -272,13 +294,18 @@ export function schemaOpDrops(schema, ops) {
272
294
  continue;
273
295
  const props = (saved.props ?? {});
274
296
  if (!Object.hasOwn(props, op.prop)) {
275
- drops.push(`replace_prop '${op.target}.${op.prop}': prop şemada yok — değer kaydedilmemiş.`);
297
+ addDrop(drops, `replace_prop '${op.target}.${op.prop}': prop şemada yok — değer kaydedilmemiş.`);
276
298
  continue;
277
299
  }
278
- for (const entry of silentlyDropped({ [op.prop]: unwrapProps(op.value) }, { [op.prop]: unwrapProps(props[op.prop]) })) {
279
- drops.push(`replace_prop '${op.target}' → ${entry}`);
280
- }
300
+ mergeNested(drops, writeDiff({ [op.prop]: unwrapProps(op.value) }, { [op.prop]: unwrapProps(props[op.prop]) }, {}, false), `replace_prop '${op.target}' → `);
281
301
  }
282
302
  }
283
- return drops.slice(0, maxDrops);
303
+ return finishReport(drops);
304
+ }
305
+ /** İç karşılaştırmanın satırlarını, sayısını ve notlarını dış rapora önekle taşır; kesme en dışta yapılır. */
306
+ function mergeNested(target, nested, prefix) {
307
+ for (const line of nested.dropped)
308
+ addDrop(target, `${prefix}${line}`);
309
+ target.droppedCountCapped = target.droppedCountCapped || nested.droppedCountCapped;
310
+ target.notes.push(...nested.notes.map(note => `${prefix}${note}`));
284
311
  }
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export const recordWriteViewParam = z.enum(['outline', 'full']).optional().describe("Yanıttaki kaydın biçimi. outline (VARSAYILAN): data yalnız kimlik alanlarını taşır (id, name, slug, version ve türüne göre url/httpMethod/source/type) — gönderdiğiniz gövdenin yankısı bağlama tekrar girmez; 'kaydedildi mi' kanıtı silentlyDropped/droppedCount'tadır. full: platformun döndürdüğü gövde aynen gelir.");
3
+ /**
4
+ * Create/update yanıtı platformun kaydettiği tanımın TAMAMINI döner; ajan aynı turda o gövdeyi
5
+ * kendisi göndermiştir. Kırpma yalnız bağımsız doğrulama yolu olan yerde yapılır: bu tool'larda
6
+ * `writeDiff` tam yanıt üzerinden ÖNCE hesaplanır, projeksiyon ondan sonra uygulanır.
7
+ */
8
+ export function outlineWrite(payload, view, keys) {
9
+ if (view === 'full' || !payload || typeof payload !== 'object' || Array.isArray(payload))
10
+ return payload;
11
+ const record = payload;
12
+ const data = record.data;
13
+ if (!data || typeof data !== 'object' || Array.isArray(data))
14
+ return payload;
15
+ const source = data;
16
+ return { ...record, data: Object.fromEntries(['id', 'name', 'slug', 'version', ...keys].filter(key => Object.hasOwn(source, key)).map(key => [key, source[key]])) };
17
+ }
@@ -34,6 +34,14 @@ component_get → tek tipin TAM sözleşmesi, üç katman birleşik:
34
34
  - **Boyut:** `blocks[].size` katalogdaki `maxSize`'ı aşamaz; yükseklikte
35
35
  `appearance.heightType: 'auto'` tercih edilir. Katalog varsayılanı bazı tiplerde kendi
36
36
  sınırını aşar — körü körüne kopyalama, gateway kırpar ve bildirir.
37
+ - **Çocuk tarafı flex sözleşmesi — `mode: 'flex'` (2026-09-18):** flex kapta kalan alanı alan
38
+ öğe komponent değil onu taşıyan düğüm sarmalayıcısıdır ve sarmalayıcının `flex`i ortak
39
+ katmanda yazılır. `mode` prop'u `'flex'` olan bir çocuk (bugün yalnız `spacer`) kabın ANA
40
+ ekseninde kalanı ister: sütunda kalan yükseklik, satırda kalan genişlik, sıfır taban (wrap
41
+ açıkken satırdan düşmez). `appearance.widthType`/`heightType`'tan bağımsızdır ve ana
42
+ eksende onları yener; kabın ölçüsü o eksende belirli olmalıdır (auto/hug sütunda kalan
43
+ alan yoktur). Komponentin kendi `style`'ından istenemez — `style`'a `flex` yazma tablosuna
44
+ bkz. aşağıda.
37
45
 
38
46
  ### Arayüz metni ve biçimler uygulama dilini izler
39
47
 
@@ -105,7 +113,7 @@ Her şema düğümünde iki sözlük vardır; ikisi de ortak render katmanında
105
113
 
106
114
  | `states` | Anlamı |
107
115
  |---|---|
108
- | `isDroppable` | İçine komponent alır (container/form/repeater true). **Değer kataloğun kararıdır** (`component_list` `droppable` sütunu): Studio kanvası bırakma hedefini bu alandan çözer, bu yüzden gateway yazdığı düğümlerde alanı katalogdan doldurur ve `gatewayNormalized` ile bildirir (PAGE-034). `false` yazılmış bir kabın içine kanvasta bırakma yapılamaz — bilerek kapatmak için `resize_component`/`set_states` |
116
+ | `isDroppable` | İçine komponent alır (container/form/repeater true). **Değer kataloğun kararıdır** (`component_list` `droppable` sütunu): Studio kanvası bırakma hedefini bu alandan çözer, bu yüzden gateway yazdığı düğümlerde alanı katalogdan doldurur ve `gatewayNormalized` ile bildirir (PAGE-036). `false` yazılmış bir kabın içine kanvasta bırakma yapılamaz — bilerek kapatmak için `resize_component`/`set_states` |
109
117
  | `isScrollable` | İçeriği kendi alanında kaydırır (sabit yükseklikte `overflow-y: auto`) |
110
118
  | `isWrapper` | Sayfanın/layout'un/widget'ın ANA kabı — salt düzen elemanı: `height:100%` + **`min-height:80vh`**, kap başlığı ÇİZİLMEZ. **Yalnız sayfa boyu kaplaması gereken tek kap için kullan:** iç içe kutuya verirsen 720px'lik pencerede o kutu 36px içerikle bile 576px çizilir (ölçüldü, `platform_guide('page')` Kural 1f). Çerçeve/başlık gizlemek için wrapper değil `style` (`border:'none'`) kullan. Kurulmuş bir düğümün bayrağını `resize_component` micro-op'unun `states` alanı değiştirir — düğümü yeniden kurmak gerekmez |
111
119
  | `isSticky` | Kaydırmada yerinde kalır (header/sidebar; z-index'i otomatik) |
@@ -105,14 +105,16 @@ globalThis["toplamHesapla"] = function (fiyat, adet) { // studio: Function
105
105
  "name": "toplamHesapla", "description": "fiyat × adet; adet verilmezse 1",
106
106
  "parameters": [ { "name": "fiyat", "type": "number", "required": true },
107
107
  { "name": "adet", "type": "number", "defaultValue": 1 } ],
108
- "body": "return fiyat * adet;" }
108
+ "body": "// fiyat × adet; adet verilmezse 1 sayılır
109
+ return fiyat * adet;" }
109
110
 
110
111
  // tarayıcı
111
112
  { "source": "studio", "applicationId": "<app id>", "scope": "application",
112
113
  "name": "toplamHesapla", "description": "fiyat × adet; adet verilmezse 1",
113
114
  "isAsync": false,
114
115
  "parameters": [ … aynı liste … ],
115
- "body": "return fiyat * adet;" }
116
+ "body": "// fiyat × adet; adet verilmezse 1 sayılır
117
+ return fiyat * adet;" }
116
118
  ```
117
119
 
118
120
  Gövde **düz deyimlerdir** — `function (…) { }` sarmalı ya da arrow yazılmaz; `return` serbesttir,
@@ -128,6 +130,33 @@ sözdizimi hatası `422 GlobalFunctionBodyInvalid` ile ve ayrıştırıcı mesaj
128
130
  | FN-004 | `source:'workflow'` + `isAsync:true` | **block** — motor senkron, 422 `GlobalFunctionAsyncNotAllowed` |
129
131
  | FN-004 | studio: `isAsync:true` ama `await` yok · `isAsync:false` ama `await` var | warn — ilki gereksiz async (inline `[object Promise]`), ikincisi **derlenmeyen gövde**: fonksiyon atlanır |
130
132
  | FN-005 | Kaynak/kapsam tutarsız: workflow'a `applicationId`/`scope`/`pageId`; studio'ya `applicationId` ya da `scope` yok; `page` kapsamına `pageId` yok; `application` kapsamına `pageId` var | **block** — platform 422 |
133
+ | FN-006 | 5+ dolu satırlı gövdede hiç yorum yok | warn — baş yorumu ekleyin (§Gövdede yorum) |
134
+
135
+ ### Gövdede yorum (FN-006)
136
+
137
+ Gövdeyi okuyan bir sonraki kişi (insan ya da ajan) fonksiyonu Studio'nun Functions drawer'ında
138
+ açar; yorumlar orada **yeşil italik** çizilir ve koddan ayrı okunur. Bu yüzden her gövde yorum taşır:
139
+
140
+ - **Baş yorumu** — ilk satırlar: fonksiyon ne yapar, parametrelerin anlamı ve birimi, ne döner.
141
+ Tek satırlık `return` gövdesinde bile bir satır yeterlidir.
142
+ - **Adım yorumu** — yalnız açık olmayan adımların üstüne: iş kuralı, sınır durumu, neden bu yol.
143
+ Kodu tekrar eden yorum yazılmaz (`// toplamı döndür` + `return toplam` gürültüdür).
144
+ - `//` kullanın; `/* */` çok satırlı açıklama için serbest. Gövde kendi satırına sarıldığı için
145
+ son satırdaki `//` kapanış parantezini yutmaz.
146
+
147
+ ```js
148
+ // KDV dahil sepet toplamı. satirlar: [{ fiyat, adet }], oran: 0.2 gibi ondalık.
149
+ // Boş sepette 0 döner; tutar kuruş hassasiyetinde yuvarlanır.
150
+ const araToplam = satirlar.reduce((toplam, satir) => toplam + satir.fiyat * satir.adet, 0)
151
+
152
+ // İndirim KDV'den ÖNCE uygulanır (muhasebe kuralı)
153
+ const indirimli = araToplam * (1 - (indirim ?? 0))
154
+
155
+ return Math.round(indirimli * (1 + oran) * 100) / 100
156
+ ```
157
+
158
+ 5 ya da daha fazla dolu satırlı ve hiç yorumu olmayan gövde **FN-006** uyarısı alır; kayıt yine
159
+ yapılır.
131
160
 
132
161
  Studio kaydı sonrası bağlı sayfaların çalışma paketi `Pages.Updated` ile yeniden kurulur (B-11):
133
162
  `/dev` yolu yeni gövdeyi hemen, `/view` yolu önbellek tazelenince gösterir. Workflow kaydı önbellek
@@ -153,6 +153,21 @@ koda baktığını gizlemek yerine raporlamak.
153
153
  (GW-005). Bir argüman kabul edildiyse gateway onu okuyor demektir.
154
154
 
155
155
 
156
+ ## Aynı çağrıyı iki kez yapmak kopya açmaz: `ifExists`
157
+
158
+ Platform aynı adla ikinci kaydı hata vermeden `"<ad> (1)"` olarak açar. `flow_create`,
159
+ `microservice_create`, `workflow_create` ve `page_create` bu yüzden yazmadan önce aynı ad ya da
160
+ slug'ı (harf duyarsız) arar:
161
+
162
+ | `ifExists` | Kayıt zaten varsa |
163
+ |---|---|
164
+ | `error` (varsayılan) | Yazmaz; hata mevcut id'yi ve varsa `(n)` sayaçlı kopyaları söyler |
165
+ | `update` | Mevcut kaydı günceller, yanıtta `outcome: 'updated'`. `page_create`'te yalnız verilen alanlar yazılır, şema korunur. `workflow_create`'te mevcut kaydın `type`'ı farklıysa (middleware ↔ automation) yazmaz — tür değişirdi |
166
+ | `duplicate` | Platform davranışı: ikinci kayıt açılır. Platformun eklediği sayaç (ad `' (1)'`, slug `-1`; akışta ikisi de `' (1)'`) kayıp değildir, yanıtta `renamedByPlatform`'dadır |
167
+
168
+ Betiği yeniden koşturacaksan `ifExists: 'update'` ver; kopyaları elle ayıklayan yardımcı kod
169
+ yazma. Mevcut liste okunamazsa çağrı yazmadan düşer — tekrar dene.
170
+
156
171
  ## Yazdıktan sonra `silentlyDropped` alanına bak
157
172
 
158
173
  Platformun bir alanı **hatasız yutması** bu depodaki en pahalı hata sınıfı: çağrı 200 döner,
@@ -169,6 +184,26 @@ Gateway yazma tool'larında gönderdiğin gövdeyi kaydedilenle karşılaştır
169
184
  ]
170
185
  ```
171
186
 
187
+ Yazma yanıtında bu liste ve `gatewayNormalized` **kısaltılmış** gelir: aynı yolun düşmeleri tek
188
+ satırda sayıyla toplanır (`selectFields[*].path · kaydedilmedi: 25 öğe — ör. …`). Kayıp sayısı
189
+ `droppedCount`'tadır; sayım 1000 farkta durursa `droppedCountCapped: true` gelir ve sayı "en az"
190
+ demektir. Liste dışı bilgiler (20 satır kesmesi, sıra tabanlı karşılaştırma uyarısı)
191
+ `silentlyDroppedNotes`'tadır ve sayıya girmez. Tam liste gerekiyorsa yazmayı tekrarlama — kaydı `*_get` ile oku,
192
+ sonraki yazmalarda `diagnostics: 'full'` ver. `microservice_*` ve `workflow_*` create/update
193
+ yanıtında `data` yalnız kimlik alanlarını taşır; gövdenin tamamı için `view: 'full'`.
194
+
195
+ **Betikten toplu yazarken yanıtı olduğu gibi basma.** Otuz yazmanın tam yanıtı çıktı sınırını aşar
196
+ ve dosyaya düşer. Her çağrı için tek satır bas; ayrıntıyı yalnız sorunlu çağrıda bas:
197
+
198
+ ```js
199
+ const r = JSON.parse(result.content[0].text)
200
+ if (result.isError || r.droppedCount > 0) console.log(JSON.stringify({ slug, error: result.isError ? r : undefined, droppedCount: r.droppedCount, silentlyDropped: r.silentlyDropped }))
201
+ else console.log(JSON.stringify({ slug, id: r.data?.id, outcome: r.outcome }))
202
+ ```
203
+
204
+ Yalnız `isError`'a bakma: platform alanı hatasız yutunca çağrı başarılıdır ve kayıp yalnız
205
+ `droppedCount`'ta görünür.
206
+
172
207
  Diff dizi öğelerini indeksle değil KİMLİKLE eşler (`stepId`, `elementId`, `name`, `id`), çünkü
173
208
  platform diziyi kendi sırasında geri döndürür. Diff üretilemeyen tek durum, yanıtın kayıt değil
174
209
  kabul gövdesi olmasıdır; o zaman alan hiç görünmez ve doğrulamayı `*_get` ile kendin yaparsın.
@@ -36,7 +36,8 @@ Mobil uygulamanın bileşen kataloğu ayrıdır (backend satırları `(slug, typ
36
36
  şema web sınıfını genişlettiği için prop'ların anlamı oradadır), `mobileKnowledge` (bu bileşenin
37
37
  mobil farkları: düşen prop, değişen etkileşim, mobile özgü prop notları) ve `mobileNotes` (bütün
38
38
  mobil bileşenlere geçen notlar). Web'de karşılığı olmayan bileşende (`app-bar`, `safe-area`,
39
- `scroll-view`, `choice-chips`, `tooltip`) bilgi yalnız `mobileKnowledge`'tadır. Olay yükleri
39
+ `scroll-view`, `choice-chips`, `tooltip`, `markdown`, `calendar`, `stack`, `list-item`, `fab`, `timer`,
40
+ `audio`, `lottie`, `webview`) bilgi yalnız `mobileKnowledge`'tadır. Olay yükleri
40
41
  (`events[].payload`) önce mobile özgü sözleşmeden okunur (`app-bar.clickAction`, `repeater.swipe`,
41
42
  `choice-chips.change`…), yoksa web'inkinden.
42
43
 
@@ -54,6 +55,30 @@ mobil bileşenlere geçen notlar). Web'de karşılığı olmayan bileşende (`ap
54
55
  uzun içerik için içine `scroll-view`). En-boy oranı yeni
55
56
  prop DEĞİLDİR: boyut panelinin `appearance.aspectRatio`'su mobilde kaplara da uygulanır
56
57
  (§Yerleşim).
58
+ İkinci adımda dört bileşen: `markdown` (Markdown çizer, HTML çalıştırmaz; bağlantı `linkClick { href }`)
59
+ ve `calendar` (satır içi takvim, tek gün ya da aralık; prop adları ve `change` biçimi web
60
+ `datepicker`ınki) mobile özgüdür; `otpinput` ve `qrcode` web ikizleridir, kuralları web modülünden.
61
+ Üçüncü adımda dört bileşen daha: `stack` (katmanlar: ilk çocuk taban, üsttekiler yığının 9 noktalı
62
+ `alignment`ıyla hizalanır — çocukta hiza alanı YOKTUR), `list-item` (satır: baş · başlık · alt başlık ·
63
+ son; `click`), `fab` (yüzen düğme: sayfanın neresinde olursa olsun ekranın alt köşesinde durur, içerikle
64
+ kaymaz; `click`, iç rol `__press`) mobile özgüdür; `carousel` web ikizidir (`activeIndex`,
65
+ `slideChange`) ve mobilde bırakılabilir — çocukları sayfalardır.
66
+ Dördüncü adım Katalog B'yi kapattı: `timer` (mobile özgü; başlat/duraklat `isRunning`, sıfırlama `value`
67
+ yazmak; `tick { value }` her saniye, `finish`), `rating`, `multi-select` (değer `{ label, value }`
68
+ dizisi) ve `toast` web ikizleridir. `toast`ın bildirimi sayfadan bağımsız yaşar (`redirect`ten sonra da
69
+ görünür), açık modal ya da çekmecenin de üstünde çizilir; akışın tipli `alert` uyarıları cihazda da
70
+ toast'tur.
71
+ - **Katalog C** (cihaz modüllü bileşenler) medyayla başladı: `video` web ikizidir (cihazda `expo-video`,
72
+ kanvas ve önizlemede HTML5 `<video>`; `click` yalnız `controls: false` iken), `audio` (oynat/duraklat,
73
+ ilerleme izi; iç roller `__toggle`, `__track`) ve `lottie` (Lottie JSON ya da `.lottie`) mobile özgüdür.
74
+ İkisinde çalma `isPlaying` prop'udur: akış `true`/`false` yazar, bileşen `play`/`pause`/`ended` ya da
75
+ `finish` olayında `{ isPlaying }` yayar ve prop'u gerçeğe döndürür.
76
+ İkinci adım: `signature` web ikizidir (her vuruşta `change { value }` PNG data URL; vuruş sürerken sayfa
77
+ kaymaz; iOS'ta itilmiş sayfada sağa vuruş sayfayı kapatır, ARC-116) ve `webview` mobile özgüdür (web
78
+ `iframe`inin karşılığı): `src` ya da `html`; yüklemeden sonra başka belgeye giden gezinme varsayılan olarak
79
+ engellenir ve `navigate { url }` yayar, adres yalnız kullanıcının dokunduğu bağlantıda sistem tarayıcısında
80
+ açılır (`allowNavigation` görünümde açar); sayfa akışa `window.ReactNativeWebView.postMessage(metin)` →
81
+ `message { data }` ile yazar, yüklenemeyen sayfa `error` yayar.
57
82
  - Web'e özgü bileşenlerin mobil karşılığı:
58
83
 
59
84
  | Web | Mobilde |
@@ -62,7 +87,7 @@ mobil bileşenlere geçen notlar). Web'de karşılığı olmayan bileşende (`ap
62
87
  | `popover` | `modal` widget'ı |
63
88
  | `pagination` | `repeater` + "daha fazla yükle" düğmesi |
64
89
  | `breadcrumb` | yığın gezinmesi (app-bar geri düğmesi) |
65
- | `iframe` | `button` + `redirect` (`targetBlank`) ile tarayıcıda aç |
90
+ | `iframe` | `webview` (`src` ya da `html`) |
66
91
  | `theme-toggle` | cihaz teması |
67
92
 
68
93
  **Harici bileşen mobilde yoktur** (`MOB-006`): protokol v1 tarayıcı `.mjs` paketidir;
@@ -167,6 +192,30 @@ etiketi ailesini `typographyStyle`'dan alır, alan etiketleri platform yazı tip
167
192
  (sekmeler), `ngapps://examples/mobile-app-shell-drawer` (çekmece); üç sayfalık liste → detay → form akışı:
168
193
  `ngapps://examples/mobile-list-detail-form`.
169
194
 
195
+ ## Çok dil
196
+
197
+ Mobil uygulama web ile **aynı sözlüğü ve aynı motoru** kullanır (plans/mobil-coklu-dil, 2026-09-25):
198
+ `project_update(languages, defaultLanguage)` → prop'u bağla (`set_prop_type(target, prop, 'translation', ref)`)
199
+ → `translation_update`. Kurulum ve fallback zinciri `platform_guide('translation')`.
200
+
201
+ - **Dil değiştirici:** `language-selector` mobil katalogdadır (`component_get` → `mobileKnowledge`):
202
+ `dropdown`un tam ekran `Modal` listesi, tetikleyici `role="combobox"`, seçenekler dilin yerel adıyla
203
+ (`tr` → Türkçe). Seçim `machine.setLanguage` ile bütün prop'ları yerinde çözer ve cihazda uygulama
204
+ çapında yayılır: öteki sekmelerin ekranları ve sekme etiketleri de değişir, makine yeniden kurulmaz.
205
+ - **Cihazda boot sırası:** uygulama içi seçim → cihazda kayıtlı seçim (`zeyrek:<app>:lang`) → cihaz dili
206
+ → projenin varsayılanı. `?lang` CİHAZDA YOKTUR; `/preview`'da vardır, bu yüzden `page_screenshot(language:)`
207
+ mobil sayfada da çalışır — kanıt için sayfaya seçici koymak gerekmez.
208
+ - **Sekme etiketi:** `properties.mobile.tabs[].label` `t:<anahtar>` ile çevrilir (doğrulama mesajıyla aynı
209
+ önek); `mobile_app_create` `tabs[].label` de kabul eder. Sayfa adı fallback'i düz kalır.
210
+ - **Çerçeve:** cihazda `alert`/`confirm` düğmeleri (`Tamam`/`Evet`/`Hayır` ↔ `OK`/`Yes`/`No`) uygulama dilini
211
+ izler; mesaj gövdesi akışın kendi metnidir.
212
+ - **Doğrulama:** `page_screenshot({ language: 'en' })` ikinci dilin karesi;
213
+ `page_screenshot({ actions: [{ select: '<seçici elementId>', value: 'en' }] })` seçiciyi sürer
214
+ (`optionsSeen` proje dili sayısıdır); seçici sonrası sekme çubuğunun `t:` etiketleri de aynı karede
215
+ çevrilir (ölçüldü 2026-09-25: `Ana/Liste`).
216
+ - **Bilinen fark:** mobilde istemci tarafı şema doğrulaması yoktur, `t:` doğrulama mesajı akışta
217
+ (`Loc()` / `t()`) çözülür.
218
+
170
219
  ## Akış adımları
171
220
 
172
221
  Motorun kendi adımları (`setter`, `condition`, `microservice`, `code`, `function`, `flow-trigger`,