@appaflytech/zeyrek-ai-mcp 0.0.6 → 0.0.8

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 (98) 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 +88 -0
  6. package/dist/knowledge/componentCatalog.js +415 -68
  7. package/dist/knowledge/componentEvents.js +120 -25
  8. package/dist/knowledge/functionGuards.js +134 -0
  9. package/dist/knowledge/normalize.js +512 -25
  10. package/dist/knowledge/outlines.js +31 -0
  11. package/dist/knowledge/policy.js +14 -5
  12. package/dist/knowledge/propTypes.js +5 -4
  13. package/dist/knowledge/rules.js +453 -6
  14. package/dist/knowledge/styleResolve.js +6 -1
  15. package/dist/platform/csv.js +123 -0
  16. package/dist/platform/errors.js +62 -3
  17. package/dist/platform/http.js +11 -2
  18. package/dist/platform/responses.js +16 -4
  19. package/dist/platform/screenshot.js +98 -9
  20. package/dist/platform/strictArguments.js +32 -0
  21. package/dist/platform/writeDiff.js +6 -3
  22. package/dist/resources/examples/flow-call-function.json +66 -0
  23. package/dist/resources/examples/function-total.json +70 -0
  24. package/dist/resources/examples/microfunction-node-sum.json +56 -0
  25. package/dist/resources/examples/page-dynamic-form.json +1 -1
  26. package/dist/resources/examples/page-sign-in.json +2 -2
  27. package/dist/resources/guides/component.md +14 -2
  28. package/dist/resources/guides/entity.md +321 -17
  29. package/dist/resources/guides/expressions.md +9 -0
  30. package/dist/resources/guides/functions.md +262 -0
  31. package/dist/resources/guides/getting-started.md +7 -0
  32. package/dist/resources/guides/layout.md +6 -0
  33. package/dist/resources/guides/merge.md +231 -0
  34. package/dist/resources/guides/microfunction.md +166 -0
  35. package/dist/resources/guides/microservice.md +166 -7
  36. package/dist/resources/guides/modify-existing.md +23 -0
  37. package/dist/resources/guides/page.md +101 -22
  38. package/dist/resources/guides/translation.md +35 -10
  39. package/dist/resources/guides/workflow.md +51 -2
  40. package/dist/resources/schema-fingerprint.json +4 -4
  41. package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
  42. package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
  43. package/dist/resources/schemas/CreateBulkRowCommand.json +3 -0
  44. package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
  45. package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
  46. package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
  47. package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
  48. package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
  49. package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
  50. package/dist/resources/schemas/CreateMicroserviceCommand.json +123 -2
  51. package/dist/resources/schemas/CreateWorkflowCommand.json +17 -3
  52. package/dist/resources/schemas/DataJobDto.json +267 -0
  53. package/dist/resources/schemas/ExportRowsCommand.json +172 -0
  54. package/dist/resources/schemas/Filter.json +3 -1
  55. package/dist/resources/schemas/GetRowsQuery.json +3 -1
  56. package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
  57. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
  58. package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
  59. package/dist/resources/schemas/ImportPlanDto.json +260 -0
  60. package/dist/resources/schemas/ImportSampleDto.json +90 -0
  61. package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
  62. package/dist/resources/schemas/PreviewMicroserviceCommand.json +123 -2
  63. package/dist/resources/schemas/PreviewWorkflowCommand.json +17 -3
  64. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
  65. package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
  66. package/dist/resources/schemas/RunImportCommand.json +62 -0
  67. package/dist/resources/schemas/StepDto.json +17 -3
  68. package/dist/resources/schemas/UpdateBulkRowCommand.json +3 -0
  69. package/dist/resources/schemas/UpdateEntityCommand.json +3 -1
  70. package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
  71. package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
  72. package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
  73. package/dist/resources/schemas/UpdateMicroserviceCommand.json +123 -2
  74. package/dist/resources/schemas/UpdateWorkflowCommand.json +17 -3
  75. package/dist/server.js +17 -4
  76. package/dist/tools/activities.js +40 -0
  77. package/dist/tools/automations.js +14 -5
  78. package/dist/tools/common.js +16 -0
  79. package/dist/tools/dependencies.js +1 -1
  80. package/dist/tools/environments.js +52 -0
  81. package/dist/tools/files.js +51 -50
  82. package/dist/tools/flows.js +104 -5
  83. package/dist/tools/functions.js +331 -0
  84. package/dist/tools/importSchemas.js +79 -0
  85. package/dist/tools/imports.js +241 -0
  86. package/dist/tools/layouts.js +4 -4
  87. package/dist/tools/merges.js +113 -0
  88. package/dist/tools/microfunctions.js +155 -0
  89. package/dist/tools/microservices.js +112 -9
  90. package/dist/tools/pages.js +188 -66
  91. package/dist/tools/platformInfo.js +4 -1
  92. package/dist/tools/queries.js +72 -6
  93. package/dist/tools/revisions.js +94 -0
  94. package/dist/tools/rows.js +342 -22
  95. package/dist/tools/screenWrite.js +11 -2
  96. package/dist/tools/templates.js +32 -5
  97. package/dist/tools/workflows.js +28 -6
  98. package/package.json +3 -1
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Gateway'in kendi minimal RFC 4180 okuyucusu. `packages/ng-apps-ui`'daki `csv-parse.ts`
3
+ * kopyalanmaz: o dosya tarayıcı için yazılmıştır ve bu proje frontend workspace'ine dahil
4
+ * değildir (sözleşme D5) — kopya iki yerde birden bozulurdu.
5
+ *
6
+ * Değerler HEP string döner. Tip dönüşümü backend'in işidir: `1.234,56` ile `1,234.56` arasındaki
7
+ * fark kültüre bağlıdır ve gateway istek kültürünü bilmez.
8
+ */
9
+ /**
10
+ * Ayraç ilk satırdan tahmin edilir. Türkçe Excel `;` ile yazar ve o dosyalarda virgül ONDALIK
11
+ * AYRACIDIR — yanlış tahmin `1,5` değerini iki sütuna böler, hem de sessizce.
12
+ */
13
+ export function detectDelimiter(firstLine) {
14
+ let semicolons = 0;
15
+ let commas = 0;
16
+ let quoted = false;
17
+ for (const character of firstLine) {
18
+ if (character === '"')
19
+ quoted = !quoted;
20
+ else if (quoted)
21
+ continue;
22
+ else if (character === ';')
23
+ semicolons += 1;
24
+ else if (character === ',')
25
+ commas += 1;
26
+ }
27
+ return semicolons > commas ? ';' : ',';
28
+ }
29
+ function stripBom(text) {
30
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
31
+ }
32
+ function firstLineOf(text) {
33
+ let quoted = false;
34
+ for (let index = 0; index < text.length; index += 1) {
35
+ const character = text[index];
36
+ if (character === '"')
37
+ quoted = !quoted;
38
+ else if (!quoted && (character === '\n' || character === '\r'))
39
+ return text.slice(0, index);
40
+ }
41
+ return text;
42
+ }
43
+ /**
44
+ * Satır satır hücre dizisi. Tırnaklı hücre ayraç ve satır sonu taşıyabilir, `""` tek tırnaktır;
45
+ * tamamen boş satırlar atılır — Excel'in dosya sonuna bıraktığı boş satır aksi hâlde her alanı
46
+ * boş bir kayıt üretirdi.
47
+ */
48
+ export function parseCsv(text, delimiter) {
49
+ const content = stripBom(text);
50
+ const separator = delimiter ?? detectDelimiter(firstLineOf(content));
51
+ const rows = [];
52
+ let row = [];
53
+ let value = '';
54
+ let quoted = false;
55
+ let index = 0;
56
+ const endValue = () => {
57
+ row.push(value);
58
+ value = '';
59
+ };
60
+ const endRow = () => {
61
+ endValue();
62
+ if (row.some(cell => cell.length > 0))
63
+ rows.push(row);
64
+ row = [];
65
+ };
66
+ while (index < content.length) {
67
+ const character = content[index];
68
+ if (quoted) {
69
+ if (character === '"') {
70
+ if (content[index + 1] === '"') {
71
+ value += '"';
72
+ index += 2;
73
+ continue;
74
+ }
75
+ quoted = false;
76
+ index += 1;
77
+ continue;
78
+ }
79
+ value += character;
80
+ index += 1;
81
+ continue;
82
+ }
83
+ if (character === '"' && value.length === 0) {
84
+ quoted = true;
85
+ index += 1;
86
+ continue;
87
+ }
88
+ if (character === separator) {
89
+ endValue();
90
+ index += 1;
91
+ continue;
92
+ }
93
+ if (character === '\r' || character === '\n') {
94
+ endRow();
95
+ index += character === '\r' && content[index + 1] === '\n' ? 2 : 1;
96
+ continue;
97
+ }
98
+ value += character;
99
+ index += 1;
100
+ }
101
+ if (value.length > 0 || row.length > 0)
102
+ endRow();
103
+ return rows;
104
+ }
105
+ /**
106
+ * Başlık satırını anahtar yapan satır sözlükleri. Başlık boş olan sütun atlanır (Excel'in sağa
107
+ * doğru uzattığı boş sütunlar aksi hâlde `''` anahtarlı bir alan üretirdi) ve fazladan hücre
108
+ * taşıyan satır kırpılır.
109
+ */
110
+ export function csvRows(text, delimiter) {
111
+ const grid = parseCsv(text, delimiter);
112
+ if (grid.length === 0)
113
+ return [];
114
+ const headers = (grid[0] ?? []).map(header => header.trim());
115
+ return grid.slice(1).map(cells => {
116
+ const row = {};
117
+ headers.forEach((header, column) => {
118
+ if (header.length > 0)
119
+ row[header] = cells[column] ?? '';
120
+ });
121
+ return row;
122
+ });
123
+ }
@@ -1,6 +1,12 @@
1
1
  export class PlatformApiError extends Error {
2
2
  status;
3
3
  code;
4
+ /**
5
+ * Toplu satır yazmasında düşen satırın parti içi indeksi (`BulkRowErrorResponse.rowIndex`).
6
+ * Mesajda da geçer ama metinden ayıklanamaz: parti döngüsü dosya satır numarasını hesaplamak
7
+ * için sayıya ihtiyaç duyar, çağıran hangi satırı düzelteceğini ancak öyle bilir.
8
+ */
9
+ rowIndex;
4
10
  constructor(message, status, code) {
5
11
  super(message);
6
12
  this.status = status;
@@ -50,6 +56,56 @@ function collectValidationMessages(payload) {
50
56
  * hatalı prop'ları sessizce kaydediyordu). Orada op listesi yoktur ve `page_schema_validate` de
51
57
  * uygulanmaz — ipucu bu yüzden yola göre ayrışır.
52
58
  */
59
+ /**
60
+ * Merge apply'ın iki 409'u da aynı şeyi söyler: plan artık uygulanabilir değil. Ajan bunu
61
+ * 'geçici hata, tekrar dene' diye okuyup aynı çağrıyı yollamasın diye yol açıkça yazılır.
62
+ */
63
+ function mergeHint(path) {
64
+ if (!/\/merges\//.test(path))
65
+ return '';
66
+ return " Üç sebepten biri: çözülmemiş çakışma, ya da plan alındığından beri DALIN veya development’ın değişmesi (MergePlanStaleException) — ikincisinde planın taşıdığı liste eskidir ve uygulansaydı sonradan yapılan iş sessizce düşerdi. merge_get ile planı yeniden okuyun (isStale), bayatsa merge_refresh ile tazeleyin — yeni plan açmanız gerekmez. Aynı merge_apply çağrısını tekrarlamak sonucu değiştirmez.";
67
+ }
68
+ function mergeRevertHint(code) {
69
+ if (code === 'MergeRevertWindowExpiredException') {
70
+ return " Geri alma penceresi doldu: pencere kapanınca merge öncesi anlık görüntü de silinir, bu yüzden plan artık geri alınamaz. merge_list status:'applied' ile pencere içindeki bir plan seçin.";
71
+ }
72
+ if (code === 'MergePlanNotRevertableException') {
73
+ return ' Bu plan bir GERİ ALMA planı (kind=revert) ve geri almanın geri alması yoktur. Geri almayı iptal etmek istiyorsanız dalı yeniden merge edin.';
74
+ }
75
+ if (code === 'MergePlanNotAppliedException') {
76
+ return " Plan uygulanmış DEĞİL — durumu mesajda yazıyor (failed / discarded / reverted / planned…). Yalnız 'applied' ve 'applied-with-drift' geri alınabilir; merge_list status:'applied' ile uygun planı seçin. Zaten geri alınmış bir planın durumu 'reverted'dır ve ikinci kez geri alınamaz.";
77
+ }
78
+ return '';
79
+ }
80
+ /**
81
+ * İçe aktarma zinciri dört çağrıdır ve her hata farklı bir adıma geri gönderir: plan süresi
82
+ * dolduysa analizden, anahtar benzersiz değilse plan gövdesinden, hata oranı yüksekse dosyanın
83
+ * kendisinden. Kod olmadan mesajlar "422" diye okunur ve ajan aynı çağrıyı tekrarlar.
84
+ */
85
+ function importHint(code, path) {
86
+ if (code === 'ImportPlanExpiredException') {
87
+ return ' Plan bir saat geçerlidir ve süresi doldu (ya da başka bir kullanıcıya ait). import_analyze ile dosyayı yeniden okuyup import_plan ile yeni bir plan üretin — aynı planId bir daha çalışmaz.';
88
+ }
89
+ if (code === 'ImportTooManyErrorsException') {
90
+ return " Satırların yarısından fazlası kuru koşuda düştü. Önce planın `errors` listesine bakın: çoğu zaman tek bir yanlış `locale` ya da yanlış sütun tipi bütün satırları düşürür. Dosya gerçekten böyleyse import_run'ı `force: true` ile çağırın.";
91
+ }
92
+ if (code === 'ImportKeyFieldNotUniqueException') {
93
+ return " Upsert/update anahtarı benzersiz olmalı: alanın kendisi unique/primary key olmalı, ya da tek alanlı bir unique index taşımalı, ya da 'id'/'code' olmalı. entity_get ile alanın `unique` bayrağını doğrulayın.";
94
+ }
95
+ if (code === 'ImportRelationMatchFieldNotUniqueException') {
96
+ return ' Ad → id eşlemesi yalnız hedef tablonun display alanı ya da benzersiz bir alanıyla yapılır; benzersiz olmayan bir alanda aynı ad iki satıra gidebilir ve hangisinin yazılacağı belirsiz olur.';
97
+ }
98
+ if (code === 'ImportSheetEmptyException' || code === 'ImportRowLimitExceededException') {
99
+ return ' `headerRow` doğru mu kontrol edin (0 = başlık yok); satır sınırı aşıldıysa dosyayı sayfalara bölün.';
100
+ }
101
+ if (code === 'DataJobNotUndoableException' && path.endsWith('/cancel')) {
102
+ return ' İş zaten bitmiş (completed / failed / undone / cancelled); iptal edilecek bir şey yok. Tamamlanmış işi geri almak için import_undo.';
103
+ }
104
+ if (code === 'DataJobNotUndoableException' && path.endsWith('/undo')) {
105
+ return ' Yalnız tamamlanmış ve undoAvailableUntil geçmemiş bir içe aktarma geri alınır; mevcut tabloya yalnız ekleme yapan işin penceresi hiç açılmaz. Süren işi durdurmak için import_cancel.';
106
+ }
107
+ return '';
108
+ }
53
109
  function pageSchemaHint(code, path) {
54
110
  if (code === 'PageSchemaVersionConflictException') {
55
111
  return ' Sayfa siz okuduktan sonra değişti ve HİÇBİR op uygulanmadı. page_schema_get ile yeniden okuyup planı tazeleyin, yeni `version` değerini `baseVersion` olarak gönderin.';
@@ -133,7 +189,7 @@ export function mapPlatformError(status, payload, context) {
133
189
  if (validationMessages.length > 0) {
134
190
  return new PlatformApiError(`Doğrulama hatası (${location}): ${validationMessages.join(' ; ')}. Gövde şeması için ilgili ngapps://schemas kaynağına ya da mevcut bir kaydın *_get çıktısına bakın.`, status, code);
135
191
  }
136
- return new PlatformApiError(`Geçersiz istek (${location}): ${apiMessage ?? 'API gövdeyi reddetti.'}`, status, code);
192
+ return new PlatformApiError(`Geçersiz istek (${location}): ${apiMessage ?? 'API gövdeyi reddetti.'}${mergeRevertHint(code)}${importHint(code, context.path)}`, status, code);
137
193
  }
138
194
  if (status === 401) {
139
195
  const hint = context.authMode === 'local'
@@ -151,7 +207,7 @@ export function mapPlatformError(status, payload, context) {
151
207
  if (context.method !== 'GET' && /\/flows(\/|$)/.test(context.path)) {
152
208
  return new PlatformApiError(`Adımda referans verilen kayıt çözümlenemedi (${location}). Bu uçta 404 akışın kendisini DEĞİL bir adım argümanını kastediyor: flow-trigger adımının args'ında ne flowId ne triggerElementId var (ya da triggerElementId sayfadaki hiçbir elementId ile eşleşmiyor), setter adımının iteratees[].element'i bulunamıyor, ya da bir adım silinmiş bir komponente bakıyor. Adım argümanlarını page_schema_get'teki elementId'lerle karşılaştırın; flow-trigger'ın iki geçerli şekli için platform_guide('page') Kural 1c.`, status, code);
153
209
  }
154
- return new PlatformApiError(`Kayıt bulunamadı (${location}).${environmentHint} id/slug değerini ilgili *_list tool'uyla doğrulayın.`, status, code);
210
+ return new PlatformApiError(`Kayıt bulunamadı (${location}).${environmentHint} id/slug değerini ilgili *_list tool'uyla doğrulayın.${importHint(code, context.path)}`, status, code);
155
211
  }
156
212
  if (status === 409 || status === 422) {
157
213
  const projectConflictHint = status === 409 && context.method === 'POST' && /\/projects\/?$/.test(context.path)
@@ -159,7 +215,10 @@ export function mapPlatformError(status, payload, context) {
159
215
  : '';
160
216
  const validationMessages = collectValidationMessages(body);
161
217
  const detail = validationMessages.length > 0 ? ` → ${validationMessages.join(' ; ')}` : '';
162
- return new PlatformApiError(`İş kuralı reddetti (${location}): ${apiMessage ?? 'çakışma'}${code ? ` [${code}]` : ''}${detail}${projectConflictHint}${pageSchemaHint(code, context.path)}${bulkRowHint(code)}${versionConflictHint(code, context.path)}`, status, code);
218
+ const rejected = new PlatformApiError(`İş kuralı reddetti (${location}): ${apiMessage ?? 'çakışma'}${code ? ` [${code}]` : ''}${detail}${projectConflictHint}${pageSchemaHint(code, context.path)}${bulkRowHint(code)}${importHint(code, context.path)}${versionConflictHint(code, context.path)}${mergeHint(context.path)}`, status, code);
219
+ if (typeof body.rowIndex === 'number')
220
+ rejected.rowIndex = body.rowIndex;
221
+ return rejected;
163
222
  }
164
223
  const fallbackValidationMessages = collectValidationMessages(body);
165
224
  const fallbackDetail = fallbackValidationMessages.length > 0 ? ` → ${fallbackValidationMessages.join(' ; ')}` : '';
@@ -42,8 +42,16 @@ function appendQuery(url, query) {
42
42
  if (value === undefined || value === null || value === '')
43
43
  continue;
44
44
  if (Array.isArray(value)) {
45
- for (const entry of value)
46
- url.searchParams.append(key, String(entry));
45
+ if (value.some(entry => entry !== null && typeof entry === 'object')) {
46
+ url.searchParams.append(key, JSON.stringify(value));
47
+ }
48
+ else {
49
+ for (const entry of value)
50
+ url.searchParams.append(key, String(entry));
51
+ }
52
+ }
53
+ else if (typeof value === 'object') {
54
+ url.searchParams.append(key, JSON.stringify(value));
47
55
  }
48
56
  else {
49
57
  url.searchParams.append(key, String(value));
@@ -125,6 +133,7 @@ export class Platform {
125
133
  ...(authorization === undefined ? {} : { authorization }),
126
134
  ...(options.environment ? { 'x-environment': options.environment } : {}),
127
135
  ...callerHeaders,
136
+ 'x-client': 'mcp',
128
137
  ...(options.mirrorAuthorizationCookie && authorization !== undefined && callerHeaders.cookie === undefined
129
138
  ? { cookie: `NG_AccessToken=${authorization.replace(/^Bearer /, '')}` }
130
139
  : {})
@@ -1,5 +1,6 @@
1
1
  import { logError, logStep } from '../observability/trace.js';
2
2
  import { PlatformApiError } from './errors.js';
3
+ import { PolicyBlockError } from '../knowledge/policy.js';
3
4
  import { maskSecrets } from './secrets.js';
4
5
  export function prune(value) {
5
6
  if (Array.isArray(value))
@@ -126,12 +127,15 @@ function asStructured(payload) {
126
127
  return payload;
127
128
  return { data: payload };
128
129
  }
129
- export function toolResult(payload, config) {
130
+ export function toolResult(payload, config, expectFailure) {
130
131
  const pruned = prune(payload);
131
132
  const { payload: safe, masked, tokenRefs } = maskSecrets(pruned);
132
133
  const runtimeErrors = runtimeErrorSummary(safe);
133
134
  if (runtimeErrors.length > 0) {
134
- logStep('akış hatası ✕', `${runtimeErrors.length} adım hata ile bitti — platform HTTP 200 döndürdüğü için çağrı başarılı görünüyordu`, runtimeErrors);
135
+ // Kasıtlı negatif test (expectFailure:true) bir kusur değil bir KANIT: çağıran beklediğini
136
+ // söylediyse SONUÇ satırı 'başarılı' yazılır ve ölçüm aracının plansız hata oranını şişirmez
137
+ // (pages.ts'teki page_screenshot expectFailure deseniyle aynı; artık her handle()'lı tool'da geçerli).
138
+ logStep('akış hatası ✕', `${runtimeErrors.length} adım hata ile bitti — platform HTTP 200 döndürdüğü için çağrı başarılı görünüyordu${expectFailure === true ? ' (expectFailure:true — kasıtlı negatif test)' : ''}`, runtimeErrors);
135
139
  }
136
140
  if (masked.length > 0) {
137
141
  logStep('maskeleme', `${masked.length} gizli alan yanıttan çıkarıldı — değer *** ile değiştirildi, yolu gatewayMasked'de`, masked);
@@ -144,6 +148,7 @@ export function toolResult(payload, config) {
144
148
  const enriched = {
145
149
  ...structured,
146
150
  ...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
151
+ ...(runtimeErrors.length > 0 && expectFailure === true ? { expectedFailure: true } : {}),
147
152
  ...(masked.length > 0 ? { gatewayMasked: masked } : {}),
148
153
  ...(Object.keys(tokenRefs).length > 0 ? { tokenRefs, tokenRefNote: "Maskelenen JWT'ler gateway içinde tutuldu; bu referansları page_screenshot / page_runtime_get 'asUser' alanına verebilirsiniz (15 dk geçerli). Jetonun kendisi yanıta ve günlüğe hiç girmez." } : {}),
149
154
  ...(truncated ? { truncated: true, truncationNote: 'Liste, yanıt boyutu sınırı nedeniyle kırpıldı; sayfalama ya da daha dar filtre kullanın.' } : {})
@@ -155,7 +160,8 @@ export function toolResult(payload, config) {
155
160
  }
156
161
  export function toolError(error) {
157
162
  logError(error);
158
- const message = error instanceof PlatformApiError
163
+ // Politika durdurması bilinçli bir karardır, "beklenmeyen" değil (33/S-5).
164
+ const message = error instanceof PlatformApiError || error instanceof PolicyBlockError
159
165
  ? error.message
160
166
  : error instanceof Error
161
167
  ? `Beklenmeyen gateway hatası: ${error.message}`
@@ -168,7 +174,13 @@ export function toolError(error) {
168
174
  export function handle(config, run) {
169
175
  return async (args) => {
170
176
  try {
171
- return toolResult(await run(args), config);
177
+ // Tool'un kendi input şeması `expectFailure: z.boolean().optional()` taşıyorsa (ör.
178
+ // microservice_execute, automation_preview) burada otomatik okunur ve toolResult'a geçirilir
179
+ // — kasıtlı negatif testler (yanlış parola, yetkisiz erişim, iş kuralı ihlali) ölçüm
180
+ // aracının plansız hata oranını şişirmez. Şemasında bu alan olmayan tool'larda args'ta
181
+ // anahtar hiç bulunmaz, davranış değişmez.
182
+ const expectFailure = args?.expectFailure === true;
183
+ return toolResult(await run(args), config, expectFailure);
172
184
  }
173
185
  catch (error) {
174
186
  return toolError(error);
@@ -1,6 +1,13 @@
1
1
  import { PlatformApiError } from './errors.js';
2
2
  import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../knowledge/componentCatalog.js';
3
3
  import { logStep } from '../observability/trace.js';
4
+ /**
5
+ * Ziyaretçinin tema tercihinin yaşadığı localStorage anahtarı — `theme-toggle` bunu yazar,
6
+ * `Theme` okur ve projenin kendi modunun önüne geçirir
7
+ * (`zeyrek-frontend/packages/ng-apps-ui/src/shared/theme/stores.ts`). Anahtar uygulama başına
8
+ * DEĞİL tarayıcı başına tektir.
9
+ */
10
+ const VIEWER_MODE_STORAGE_KEY = 'zeyrek-viewer-theme-mode';
4
11
  /**
5
12
  * G-24 — gateway sayfayı kurabiliyor, okuyabiliyor ve çizdirebiliyordu ama üzerinde OLAY
6
13
  * TETİKLEYEMİYORDU: "filtre değişince veri yenileniyor" iddiası yalnız şema okumasıyla
@@ -26,11 +33,54 @@ export function actionScript(action) {
26
33
  return { found: true, applied: true, requests: count };
27
34
  })()`;
28
35
  }
36
+ // G-09 — kanban kartı ve takvim etkinliği HTML5 DnD ile taşınır. Olaylar sentetik atılır ve
37
+ // TEK DataTransfer'i paylaşır; koordinat bırakma hedefinin merkezidir, çünkü takvimin saat
38
+ // kolonu bırakılan saati `clientY`'den hesaplar (slot hedefi o saatin ortasına düşer).
39
+ // Playwright `dragTo` gerçek fare sürer ama ScreenshotPage bu yeteneği taşımaz; betik yolu
40
+ // öteki eylemlerle aynı ölçüm döngüsünden (requests · flowRuns) geçer.
41
+ if (action.action === 'drag') {
42
+ const destination = JSON.stringify(`[data-element-id="${String(action.to ?? '')}"]`);
43
+ const destinationName = JSON.stringify(String(action.to ?? ''));
44
+ return `(function () {
45
+ var count = performance.getEntriesByType('resource').length;
46
+ var node = document.querySelector(${selector});
47
+ if (!node) return { found: false, applied: false, requests: count };
48
+ var drop = document.querySelector(${destination});
49
+ if (!drop) return { found: false, applied: false, note: "sürükleme kaynağı bulundu ama bırakma hedefi ('to') sayfada yok: " + ${destinationName} + " — kanban kolonu <elementId>__col-<value>, takvim günü <elementId>__day-<yyyy-MM-dd>, saat dilimi <elementId>__slot-<yyyy-MM-dd>T<HH>", requests: count };
50
+ if (node.getAttribute('draggable') !== 'true') return { found: true, applied: false, note: "kaynak sürüklenebilir değil (draggable != true) — kanban isDraggable ya da eventcalendar isEditable kapalı olabilir", requests: count };
51
+ var transfer = new DataTransfer();
52
+ var center = function (element) { var rect = element.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; };
53
+ var fire = function (element, type, at) { element.dispatchEvent(new DragEvent(type, { bubbles: true, cancelable: true, composed: true, dataTransfer: transfer, clientX: at.x, clientY: at.y })); };
54
+ var from = center(node);
55
+ var to = center(drop);
56
+ fire(node, 'dragstart', from);
57
+ fire(drop, 'dragenter', to);
58
+ fire(drop, 'dragover', to);
59
+ fire(drop, 'drop', to);
60
+ fire(node, 'dragend', to);
61
+ return { found: true, applied: true, requests: count };
62
+ })()`;
63
+ }
64
+ const target = String(action.target ?? '');
65
+ const root = rootElementId(target);
66
+ // G-14 — `applied` yalnız "DOM'a yazıldı" demekti: tarih seçici olayı dinlemiyordu, kutu dolu
67
+ // görünüyor, prop boş kalıyordu. Kökün prop parmak izi eylemden ÖNCE alınır; runActions
68
+ // yerleşmeden sonra yeniden okuyup `propWritten`'ı çıkarır. Çok alanlı bileşenin kökünde tek
69
+ // değer yoktur — ilk kutuya yazmak yarım değeri "uygulandı" diye raporluyordu, kök reddedilir.
29
70
  return `(function () {
30
71
  var count = performance.getEntriesByType('resource').length;
31
72
  var node = document.querySelector(${selector});
32
73
  if (!node) return { found: false, applied: false, requests: count };
33
74
  var wanted = String(${value});
75
+ var propsBefore = ${runtimePropsScript(root)};
76
+ var finish = function (result) { if (propsBefore !== undefined) result.propsBefore = propsBefore; return result; };
77
+
78
+ if (${JSON.stringify(root === target)}) {
79
+ var parts = Array.prototype.slice.call(node.querySelectorAll(${JSON.stringify(`[data-element-id^="${root}${INTERNAL_ROLE_SEPARATOR}"]`)}))
80
+ .filter(function (part) { return part.querySelector('input, select, textarea'); })
81
+ .map(function (part) { return part.getAttribute('data-element-id'); });
82
+ if (parts.length > 1) return finish({ found: true, applied: false, note: "çok alanlı bileşen: kökte tek değer yok, değeri kutulardan birine yazın — " + parts.join(' · '), requests: count });
83
+ }
34
84
 
35
85
  // Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak
36
86
  // hiçbir şey yapmaz, seçeneğe TIKLANMASI gerekir. Liste ancak kutu açıkken
@@ -48,29 +98,46 @@ export function actionScript(action) {
48
98
  var match = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0]
49
99
  || options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
50
100
  if (!match) {
51
- resolve({ found: true, applied: false, note: "açılır kutu açıldı ama bu değere karşılık gelen seçenek yok: " + wanted + " — options listesini page_runtime_get ile doğrulayın; seçenek sayısı: " + options.length, requests: count });
101
+ resolve(finish({ found: true, applied: false, note: "açılır kutu açıldı ama bu değere karşılık gelen seçenek yok: " + wanted + " — options listesini page_runtime_get ile doğrulayın; seçenek sayısı: " + options.length, requests: count }));
52
102
  return;
53
103
  }
54
104
  (match.querySelector('button') || match).click();
55
- resolve({ found: true, applied: true, requests: count });
105
+ resolve(finish({ found: true, applied: true, requests: count }));
56
106
  }, 300);
57
107
  });
58
108
  }
59
109
 
60
110
  var field = node.matches('input, select, textarea') ? node : node.querySelector('input, select, textarea');
61
- if (!field) return { found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count };
111
+ if (!field) return finish({ found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count });
62
112
  var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
63
113
  : (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
64
114
  if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
65
115
  var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
66
116
  if (field.checked !== shouldCheck) field.click();
67
- return { found: true, applied: true, requests: count };
117
+ return finish({ found: true, applied: true, requests: count });
68
118
  }
69
119
  var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
70
120
  setter.call(field, ${value});
71
121
  field.dispatchEvent(new Event('input', { bubbles: true }));
72
122
  field.dispatchEvent(new Event('change', { bubbles: true }));
73
- return { found: true, applied: true, requests: count };
123
+ return finish({ found: true, applied: true, requests: count });
124
+ })()`;
125
+ }
126
+ /** `rf_range__start` → `rf_range`: iç rol hedefinin prop'ları kök düğümde yaşar. */
127
+ export function rootElementId(target) {
128
+ const separator = target.indexOf(INTERNAL_ROLE_SEPARATOR);
129
+ return separator > 0 ? target.slice(0, separator) : target;
130
+ }
131
+ /**
132
+ * Düğümün runtime prop'larının parmak izi (`@ng-apps/flow` → flow-runtime-props.ts,
133
+ * `window.__ngAppsReadProps`). `undefined` okuyucu yayınlanmıyor (eski runtime) ya da prop'lar
134
+ * serileşmiyor; `null` hiçbir makine düğümü tanımıyor; dize karşılaştırılabilir iz.
135
+ */
136
+ export function runtimePropsScript(elementId) {
137
+ return `(function () { /* runtimePropsSnapshot */
138
+ var read = window.__ngAppsReadProps;
139
+ if (typeof read !== 'function') return undefined;
140
+ try { var props = read(${JSON.stringify(elementId)}); return props === null ? null : JSON.stringify(props); } catch (error) { return undefined; }
74
141
  })()`;
75
142
  }
76
143
  /**
@@ -421,19 +488,31 @@ export class StudioScreenshotter {
421
488
  : navigated
422
489
  ? activityAfter.completed
423
490
  : activityBefore === null ? undefined : Math.max(activityAfter.completed - activityBefore.completed, 0);
491
+ // G-14 — DOM'a yazmak değerin prop'a ulaştığını kanıtlamaz. İz yalnız okuyucu yayınlanıyor ve
492
+ // makine düğümü tanıyorsa (dize) karşılaştırılır; aksi hâlde propWritten bilinmez kalır.
493
+ const propsAfter = typeof before.propsBefore === 'string' && before.applied === true && !navigated
494
+ ? await page.evaluate(runtimePropsScript(rootElementId(String(action.target ?? '')))).catch(() => undefined)
495
+ : undefined;
496
+ const propWritten = typeof propsAfter === 'string' ? propsAfter !== before.propsBefore : undefined;
497
+ const applied = before.applied === true && propWritten !== false;
498
+ const note = propWritten === false
499
+ ? `değer DOM'a yazıldı ama '${rootElementId(String(action.target ?? ''))}' düğümünün runtime prop'ları DEĞİŞMEDİ — bileşen olayı kendi change yoluna çevirmedi ya da değer zaten aynıydı. Çok alanlı bileşende <elementId>__<rol> kutusunu hedefleyin (component_get → internalRoles).`
500
+ : before.note;
424
501
  outcomes.push({
425
502
  index,
426
503
  action: String(action.action),
427
504
  ...(action.target === undefined ? {} : { target: action.target }),
505
+ ...(action.to === undefined ? {} : { to: action.to }),
428
506
  found: before.found === true,
429
- applied: before.applied === true,
430
- ...(before.note === undefined ? {} : { note: before.note }),
507
+ applied,
508
+ ...(propWritten === undefined ? {} : { propWritten }),
509
+ ...(note === undefined ? {} : { note }),
431
510
  requests,
432
511
  ...(flowRuns === undefined ? {} : { flowRuns }),
433
512
  pageState: readiness.state
434
513
  });
435
514
  }
436
- logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
515
+ logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''}${outcome.to ? ` ⇒ '${outcome.to}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
437
516
  return outcomes;
438
517
  }
439
518
  /**
@@ -505,7 +584,7 @@ export class StudioScreenshotter {
505
584
  });
506
585
  context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: 1 });
507
586
  }
508
- logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · gateway jetonu YOK'}${(request.cookies ?? []).length > 0 ? ` · caller çerezleri: ${(request.cookies ?? []).map(cookie => cookie.name).join(', ')}` : ''}${request.token || (request.cookies ?? []).length > 0 ? '' : ' · ANONİM (çerez YOK)'}`);
587
+ logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · gateway jetonu YOK'}${(request.cookies ?? []).length > 0 ? ` · caller çerezleri: ${(request.cookies ?? []).map(cookie => cookie.name).join(', ')}` : ''}${request.token || (request.cookies ?? []).length > 0 ? '' : ' · ANONİM (çerez YOK)'}${request.colorScheme ? ` · tema ${request.colorScheme}` : ''}`);
509
588
  try {
510
589
  const cookies = new Map();
511
590
  if (request.token)
@@ -515,6 +594,16 @@ export class StudioScreenshotter {
515
594
  if (cookies.size > 0) {
516
595
  await context.addCookies([...cookies].map(([name, value]) => ({ name, value, url: request.cookieUrl, httpOnly: false })));
517
596
  }
597
+ // Tema tercihi çerezde değil localStorage'da yaşar ve tema store'u değerini modül İLK
598
+ // değerlendirildiğinde okur: sayfa açıldıktan sonra yazmak bir sonraki yüklemeye kadar
599
+ // etkisizdir. Bu yüzden tercih, sayfanın script'lerinden önce koşan bağlam script'iyle
600
+ // ekilir. Yeteneği olmayan bağlamda çağrı SESSİZCE açık temada kare almaz, hata verir.
601
+ if (request.colorScheme) {
602
+ if (!context.addInitScript) {
603
+ throw new PlatformApiError(`colorScheme='${request.colorScheme}' uygulanamadı: tarayıcı bağlamı addInitScript desteklemiyor. Tema tercihi sayfa açılmadan ekilmek zorunda; desteksiz bağlamda kare projenin varsayılan modunda çıkardı ve bu sessizce yanlış olurdu.`);
604
+ }
605
+ await context.addInitScript(`try { localStorage.setItem(${JSON.stringify(VIEWER_MODE_STORAGE_KEY)}, ${JSON.stringify(request.colorScheme)}) } catch (error) { /* localStorage kapalıysa proje varsayılanı çizilir */ }`);
606
+ }
518
607
  const page = await context.newPage();
519
608
  // Soğuk Vite derlemesinde sayfanın kendi veri isteği 15 sn'yi aşabiliyor; delayMs bu
520
609
  // bütçeyi de uzatır (09-02 E-2). Yanıt hazırlık döngüsünü BLOKLAMAZ: runtime dışına
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ function isRawShape(value) {
3
+ if (!value || typeof value !== 'object' || '_def' in value)
4
+ return false;
5
+ return Object.values(value).every(entry => entry instanceof z.ZodType);
6
+ }
7
+ /**
8
+ * GW-005 — tanınmayan üst düzey argüman REDDEDİLİR.
9
+ *
10
+ * SDK ham şekli `z.object(shape)` ile sarar ve zod'un varsayılanı bilinmeyen anahtarı hata vermeden
11
+ * ATMAKTIR: şemada olmayan bir alan doğrulamadan geçer, handler'a hiç ulaşmaz ve çağrı başarıyla
12
+ * biter. 2026-09-15 G-13'te `microservice_execute`/`microservice_debug`/`query_preview`'a verilen
13
+ * `parameters` böyle kayboldu — dört farklı filtre aynı gövdeyi döndü ve ölçüm aracı çağıranı
14
+ * olmayan bir sorgu kusuruna yönlendirdi. Tek tek tool'lara alan eklemek sınıfı kapatmaz; sessizlik
15
+ * burada, SDK'ya şema verilmeden önce kaldırılır.
16
+ */
17
+ export function rejectUnknownArguments(server) {
18
+ const registerTool = server.registerTool.bind(server);
19
+ server.registerTool = ((name, config, callback) => {
20
+ if (!isRawShape(config.inputSchema))
21
+ return registerTool(name, config, callback);
22
+ const shape = config.inputSchema;
23
+ const accepted = Object.keys(shape).join(', ');
24
+ const inputSchema = z.object(shape, {
25
+ errorMap: (issue, context) => issue.code === 'unrecognized_keys'
26
+ ? { 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
+ : { message: context.defaultError }
28
+ }).strict();
29
+ return registerTool(name, { ...config, inputSchema }, callback);
30
+ });
31
+ return server;
32
+ }
@@ -1,3 +1,4 @@
1
+ import { PROP_VALUE_TYPES } from '../knowledge/normalize.js';
1
2
  import { logStep } from '../observability/trace.js';
2
3
  const maxDrops = 20;
3
4
  const maxDepth = 12;
@@ -214,9 +215,11 @@ export function unwrapProps(value) {
214
215
  const record = value;
215
216
  const isProp = typeof record.type === 'string' && (Object.hasOwn(record, 'data') || Object.hasOwn(record, 'ref'));
216
217
  if (isProp) {
217
- // `code` ve `translation` prop'larında taşıyıcı alan `ref`'tir; `data` yalnız tasarım zamanı
218
- // yedeğidir (platform_guide('translation')).
219
- const carrier = record.type === 'code' || record.type === 'translation'
218
+ // Platform `static` dışındaki her tipte değeri `ref`'e yazar (Prop ctor'u,
219
+ // DesignComponentManagementSchema.cs:666); `data` ya boştur ya türetilmiştir (file URL'i, page
220
+ // slug'ı, translation tasarım yedeği). `data`'yı taşıyıcı saymak bağlı akışı her yazmada
221
+ // "kaydedilen null" gösteriyordu (S-7).
222
+ const carrier = record.type !== 'static' && PROP_VALUE_TYPES.has(record.type)
220
223
  ? (Object.hasOwn(record, 'ref') ? record.ref : record.data)
221
224
  : (Object.hasOwn(record, 'data') ? record.data : record.ref);
222
225
  return unwrapProps(carrier);
@@ -0,0 +1,66 @@
1
+ {
2
+ "_meta": {
3
+ "synthetic": true,
4
+ "note": "Bir butonun click olayına bağlanan akış: 'function' adımı kullanıcı fonksiyonunu çağırır (fiyat komponent prop'undan, adet sabit), sonraki setter sonucu Steps.<adımAdı>.data'dan okuyup metne yazar. Canlı kanıt: 2026-09-08/09 fonksiyon-demo sayfası (kabul senaryosu 2 → 30). Şekil backend StepManagementSchema + ng-apps-flow tasks/function/function.ts okunarak kuruldu.",
5
+ "harvestedFrom": "kod + canlı doğrulama: zeyrek-frontend packages/ng-apps-flow/src/tasks/function, zeyrek-backend Contexts/Studio/Flows/StepArgument (UiFunctionId/UiFunctionName) (2026-09-09)"
6
+ },
7
+ "_rules": {
8
+ "functionId": "args.functionId ZORUNLU ve uygulamada var olmalı — backend 422 UiFunctionNotFound, gateway FLOW-024 (block). Id'yi function_list'ten alın. Runtime fonksiyonu ÖNCE id ile çözer (K9); functionName gösterim ve yedektir.",
9
+ "parameters": "parameters[].name bildirimdeki parametre adıyla eşleşir; value şekli microservice adımıyla aynı (static · property · storage · state · variable · code · page · item). Eksik zorunlu parametre adımı 'Functions.<ad>: \"<param>\" is required' ile düşürür; tanınmayan ad sessizce düşer — FLOW-024 ikisini de uyarır.",
10
+ "output": "Çıktı Steps.<adım ADI>.data (stepId değil — machine.setStorage(task.name, …)); hata Steps.<ad>.error.message (nesne). Setter'ın storage okuması mapping.source: 'hesapla.data' ile yapılır (setter.ts:161 mapping.source doluysa value.ref okunmaz).",
11
+ "branchName": "Dal adı 'always' (motor anahtarı, FLOW-004). Son adımın dalı nextStepId: null ile biter; 'end' adımı koyma (FLOW-023).",
12
+ "binding": "Akışı olaya bağlamak ayrı adımdır: page_schema_patch set_event { target: 'btn_hesapla', event: 'click', eventValue: '<flow id>' }.",
13
+ "async": "isAsync=true fonksiyon bu adımdan güvenle çağrılır (Promise beklenir); {{ }} bağlamasında ise '[object Promise]' olur."
14
+ },
15
+ "flowCreateArgs": {
16
+ "host": "page",
17
+ "hostId": "<page id>",
18
+ "name": "btn_hesapla - click",
19
+ "slug": "btn-hesapla-click",
20
+ "steps": [
21
+ {
22
+ "stepId": "start",
23
+ "name": "start",
24
+ "type": "start",
25
+ "kind": "normal",
26
+ "args": {},
27
+ "branches": [ { "name": "always", "nextStepId": "hesapla" } ]
28
+ },
29
+ {
30
+ "stepId": "hesapla",
31
+ "name": "hesapla",
32
+ "type": "function",
33
+ "kind": "normal",
34
+ "args": {
35
+ "functionId": "<function_list id — toplamHesapla>",
36
+ "functionName": "toplamHesapla",
37
+ "parameters": [
38
+ { "name": "fiyat", "value": { "type": "property", "ref": { "source": "component", "element": "num_fiyat", "property": "value" } } },
39
+ { "name": "adet", "value": { "type": "static", "ref": 3 } }
40
+ ]
41
+ },
42
+ "branches": [ { "name": "always", "nextStepId": "yaz" } ]
43
+ },
44
+ {
45
+ "stepId": "yaz",
46
+ "name": "yaz",
47
+ "type": "setter",
48
+ "kind": "normal",
49
+ "args": {
50
+ "iteratees": [
51
+ {
52
+ "type": "change",
53
+ "element": "txt_sonuc",
54
+ "property": "content",
55
+ "source": "component",
56
+ "mapping": { "source": "hesapla.data", "mappings": {} },
57
+ "value": { "type": "storage", "ref": "" }
58
+ }
59
+ ]
60
+ },
61
+ "branches": [ { "name": "always", "nextStepId": null } ]
62
+ }
63
+ ]
64
+ },
65
+ "bindEvent": { "op": "set_event", "target": "btn_hesapla", "event": "click", "eventValue": "<flow id>" }
66
+ }