@appaflytech/zeyrek-ai-mcp 0.0.11 → 0.0.13

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.
@@ -1,5 +1,25 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  const ERROR_STEP_NAME = 'gatewayOnError';
3
+ const ERROR_STEP_NAME_PATTERN = /^gatewayOnError\d*$/;
4
+ /**
5
+ * Global-error dalında `error` = `WorkflowError.ToObject()`: `{ code, message, detail, type,
6
+ * step: { id, name, type, data, input } }` (`ActivityContext.EnterGlobalErrorHandling`). Gövde
7
+ * `step`'i ALT KÜME olarak yazar, çünkü (1) `ResponseStep` global-error kipinde gövdedeki `step`
8
+ * `error.step` ile birebir aynıysa onu siler, (2) `step.input` adımın ifade değerlerini (parola
9
+ * dahil) taşır. Zincir önce `error.cause`'tan okunur: backend 2026-09-17'den beri her seviyeyi
10
+ * `{ code, message, detail, type, step: { name, type }, cause }` olarak orada verir — loop iterasyonu
11
+ * (`<loop>[<sıra>]`) ve istisnasız 2xx dışı dönen çocuk dahil (`WorkflowError.ToObject`). Eski backend'de
12
+ * `error.cause` yoktur; o zaman yalnız patlayan `workflow` adımının `step.data`'sı okunur: fırlatan
13
+ * çocukta `WorkflowError.ToResponseObject()`, 2xx dışı dönen çocukta çocuğun KENDİ yanıtı. İkincisi
14
+ * keyfî olabilir (elle yazılmış `({ err: error })` input taşır), bu yüzden `pick` her iki kaynağı seviye
15
+ * seviye beyaz listeye süzer: `code/message/detail/type/step{name,type}/cause`. Hiçbiri yoksa `cause`
16
+ * yazılmaz. Loop adımının `data`'sı tamamlanan öğelerdir — okunmaz. Canlı ölçüm 2026-09-16 (lokal `emrah/web`, üç seviyeli
17
+ * zincir + `/m/`). ES5: motor Jint 4 ama desteklenen yüzey belgesiz (RFQ W-4); null alan yazılmaz.
18
+ */
19
+ export const GATEWAY_ERROR_BODY_EXPRESSION = "(function () { function pick(source, depth) { if (!source || typeof source !== 'object') return null; var picked = {}; if (source.code) picked.code = source.code; if (source.message) picked.message = source.message; if (source.detail) picked.detail = source.detail; if (source.type) picked.type = source.type; if (source.step && source.step.name) { picked.step = { name: source.step.name }; if (source.step.type) picked.step.type = source.step.type; } if (depth < 8 && source.cause) { var inner = pick(source.cause, depth + 1); if (inner) picked.cause = inner; } return picked.code || picked.message || picked.step || picked.cause ? picked : null; } var body = { code: error.code, message: error.message }; if (error.detail) body.detail = error.detail; if (error.type) body.type = error.type; if (error.step) body.step = { name: error.step.name, type: error.step.type }; var cause = pick(error.cause, 0) || (error.step && error.step.type === 'workflow' ? pick(error.step.data, 0) : null); if (cause) body.cause = cause; return body; })()";
20
+ export const LEGACY_GATEWAY_ERROR_BODY_EXPRESSION = '({ code: error.code, message: error.message })';
21
+ const ERROR_STEP_DESCRIPTION = "Gateway'in eklediği hata yolu: akışın herhangi bir adımı patlarsa buraya düşer ve 500 ile { code, message, detail, type, step: { name, type } } döner; patlayan adım bir alt akışsa çocuğun hata gövdesi cause içinde iç içe gelir. Kendi hata gövdeni istiyorsan bu adımın bodyExpression'ını değiştir.";
22
+ const LEGACY_ERROR_STEP_DESCRIPTION = "Gateway'in eklediği hata yolu: akışın herhangi bir adımı patlarsa buraya düşer ve 500 ile { code, message } döner. Kendi hata gövdeni istiyorsan bu adımın bodyExpression'ını değiştir.";
3
23
  const INJECTED_KINDS = new Set(['workflow', 'middleware']);
4
24
  function asSteps(value) {
5
25
  return (Array.isArray(value) ? value : []).filter((step) => Boolean(step) && typeof step === 'object');
@@ -35,8 +55,9 @@ export function ensureWorkflowErrorPath(definition, kind) {
35
55
  if (steps.some(step => step !== startStep && (Array.isArray(step.branches) ? step.branches : []).some(branch => branch?.type === 'global-error')))
36
56
  return [];
37
57
  const branches = Array.isArray(startStep.branches) ? startStep.branches : [];
38
- if (branches.some(branch => branch?.type === 'global-error' && branch.nextStepId))
39
- return [];
58
+ const wired = branches.find(branch => branch?.type === 'global-error' && branch.nextStepId);
59
+ if (wired)
60
+ return upgradeLegacyErrorBody(steps, String(wired.nextStepId));
40
61
  const name = availableName(steps);
41
62
  const errorStepId = randomUUID();
42
63
  const unwired = branches.find(branch => branch?.type === 'global-error');
@@ -44,12 +65,12 @@ export function ensureWorkflowErrorPath(definition, kind) {
44
65
  id: errorStepId,
45
66
  name,
46
67
  type: 'response',
47
- description: "Gateway'in eklediği hata yolu: akışın herhangi bir adımı patlarsa buraya düşer ve 500 ile { code, message } döner. Kendi hata gövdeni istiyorsan bu adımın bodyExpression'ını değiştir.",
68
+ description: ERROR_STEP_DESCRIPTION,
48
69
  caution: "Bu adımı gateway ekledi; silersen akışın hata yolu kaybolur ve patlayan adım istemciye ham hata döndürür.",
49
70
  rule: {
50
71
  stepType: 'response',
51
72
  httpStatusCode: 500,
52
- bodyExpression: '({ code: error.code, message: error.message })'
73
+ bodyExpression: GATEWAY_ERROR_BODY_EXPRESSION
53
74
  },
54
75
  branches: []
55
76
  });
@@ -60,5 +81,27 @@ export function ensureWorkflowErrorPath(definition, kind) {
60
81
  branches.push({ id: randomUUID(), name: 'global-error', type: 'global-error', nextStepId: errorStepId });
61
82
  }
62
83
  startStep.branches = branches;
63
- return [`hata yolu eklendi: start adımına 'global-error' dalı + '${name}' response adımı (500, { code, message }). Hata yolu olmayan akış patladığında platform WorkflowStepFailedException fırlatır (HTTP 500 { code, message, detail, type }; zamanlanmış işte failed + "Step 'x' failed: …"); bu adım çıktıyı { code, message } ile denetim altına alır, zamanlanmış işte sonuç yine failed düşer. Kendi hata gövdenizi istiyorsanız bu adımın bodyExpression'ını değiştirin.`];
84
+ return [`hata yolu eklendi: start adımına 'global-error' dalı + '${name}' response adımı (500, { code, message, detail, type, step: { name, type }, cause }). Hata yolu olmayan akış patladığında platform WorkflowStepFailedException fırlatır (HTTP 500 { code, message, detail, type }, 2026-09-17 backend'inde step ve cause da; zamanlanmış işte failed + "Step 'x' failed: …"); bu adım aynı şekli ele alınmış 500 olarak döner — patlayan adımın adı ve tipi, alt akış ya da loop iterasyonu zinciri cause içinde — ve zamanlanmış işte sonuç yine failed düşer. Kendi hata gövdenizi istiyorsanız bu adımın bodyExpression'ını değiştirin.`];
85
+ }
86
+ /**
87
+ * Bu dosyanın 2026-09-16 öncesi sürümü hata adımına `{ code, message }` yazıyordu; o kayıtlar
88
+ * `detail`, `type` ve patlayan adımı dışarı vermez. Yalnız gateway'in KENDİ yazdığı ve kimsenin
89
+ * dokunmadığı adım yükseltilir: global-error dalının hedefi, adı `gatewayOnError[n]`, tipi
90
+ * `response`, ifadesi eski varsayılanla birebir aynı VE açıklaması eski gateway metni (ya da boş).
91
+ * Açıklama şartı, yükseltilmiş bir adımı aynı çağrıda bilerek eski gövdeye döndüren kullanıcıyı
92
+ * korur: yükseltme açıklamayı da yenilediği için geri dönüş bir daha ezilmez.
93
+ */
94
+ function upgradeLegacyErrorBody(steps, errorStepId) {
95
+ const target = steps.find(step => String(step.id ?? '') === errorStepId);
96
+ const rule = target?.rule;
97
+ if (!target || target.type !== 'response' || !ERROR_STEP_NAME_PATTERN.test(String(target.name ?? '')) || !rule)
98
+ return [];
99
+ if (typeof rule.bodyExpression !== 'string' || rule.bodyExpression.trim() !== LEGACY_GATEWAY_ERROR_BODY_EXPRESSION)
100
+ return [];
101
+ const untouchedDescription = target.description === undefined || target.description === null || String(target.description).trim() === '' || target.description === LEGACY_ERROR_STEP_DESCRIPTION;
102
+ if (!untouchedDescription)
103
+ return [];
104
+ rule.bodyExpression = GATEWAY_ERROR_BODY_EXPRESSION;
105
+ target.description = ERROR_STEP_DESCRIPTION;
106
+ return [`hata gövdesi genişletildi: '${String(target.name)}' adımının bodyExpression'ı gateway'in eski varsayılanıydı ({ code, message }); yeni varsayılan detail, type, patlayan adımın { name, type } bilgisini ve adım bir alt akışsa çocuğun hata gövdesini cause olarak da döner. Çocuk gövdesi yalnız code/message/detail/type/step{name,type}/cause alanlarına süzülür. Yanıta alan EKLENİR, mevcut code/message değişmez. Açıklaması ya da ifadesi elle değiştirilmiş hata adımına dokunulmaz; eski gövdede kalmak için bu adımın açıklamasını değiştirin.`];
64
107
  }
@@ -140,6 +140,49 @@ export function normalizeMicroserviceDefinition(definition) {
140
140
  return [];
141
141
  return normalizeTableParameters(definition.sourceParameters, definition.entityId);
142
142
  }
143
+ /**
144
+ * S-2: `microservice_patch`in yamaladığı belge YAZMA komutu değil OKUMA şeklidir
145
+ * (`MicroserviceDto`) — rol listesi orada `roles: [{ id, name }]`tır ve `UpdateMicroserviceCommand.FromDto`
146
+ * onu `RoleIds`'e eşler. Yazma gövdesinin adı (`roleIds`) patch belgesinde YOKTUR: `replace` yolu
147
+ * `OPERATION_PATH_UNRESOLVABLE` ile düşer, `add` ise platformda sessizce yutulur
148
+ * (`StJsonOptions.Read` bilinmeyen üyeyi atar, 200 döner, rol değişmez). Ajan yazma adını öğrenmiş
149
+ * olarak geldiği için yolu reddetmek yerine ÇEVİRİYORUZ ve çeviriyi `gatewayNormalized` ile
150
+ * söylüyoruz — istek geçer, doğru ad öğrenilir.
151
+ */
152
+ export function normalizeMicroservicePatchOps(operations, definition) {
153
+ const notes = [];
154
+ const rolesIsArray = Array.isArray(definition.roles);
155
+ for (const operation of operations) {
156
+ const path = typeof operation.path === 'string' ? operation.path : '';
157
+ if (path !== '/roleIds' && !path.startsWith('/roleIds/') && path !== '/roles' && !path.startsWith('/roles/'))
158
+ continue;
159
+ if (path === '/roleIds' || path.startsWith('/roleIds/')) {
160
+ operation.path = `/roles${path.slice('/roleIds'.length)}`;
161
+ notes.push(`patch yolu '${path}' → '${operation.path}': yamalanan belge microservice_get'in OKUMA şeklidir (MicroserviceDto) ve rol listesi orada 'roles' [{ id, name }] alanıdır; 'roleIds' yalnız create/update GÖVDESİNDE vardır. Çevrilmeseydi replace çözülemez, add ise platformda sessizce yutulurdu.`);
162
+ }
163
+ const targetPath = operation.path;
164
+ if (targetPath === '/roles' && Array.isArray(operation.value)) {
165
+ const bare = operation.value.filter(entry => typeof entry === 'string');
166
+ if (bare.length > 0) {
167
+ operation.value = operation.value.map(entry => typeof entry === 'string' ? { id: entry } : entry);
168
+ notes.push(`'${targetPath}' değerindeki ${bare.length} çıplak guid { id } nesnesine sarıldı — rol listesi id/ad çiftleri taşır, 'name' isteğe bağlıdır (platform yalnız id okur).`);
169
+ }
170
+ }
171
+ if (targetPath.endsWith('/-') && typeof operation.value === 'string') {
172
+ operation.value = { id: operation.value };
173
+ notes.push(`'${targetPath}' değeri { id } nesnesine sarıldı — listeye eklenen öğe rol kaydıdır, çıplak guid değil.`);
174
+ }
175
+ // `roles` null gelirse (platform boş listeyi null serileştirebiliyor) diziye ekleme yolu
176
+ // çözülemez: tek öğelik tam dizi yazmaya katlanır, yoksa istek gitmeden düşerdi.
177
+ if (!rolesIsArray && targetPath === '/roles/-') {
178
+ operation.op = 'add';
179
+ operation.path = '/roles';
180
+ operation.value = [operation.value];
181
+ notes.push("okunan kayıtta 'roles' dizi değil (null) — '/roles/-' eklemesi tek öğelik tam dizi yazmaya ('add /roles') katlandı.");
182
+ }
183
+ }
184
+ return notes;
185
+ }
143
186
  export function withNormalization(payload, fixes) {
144
187
  if (fixes.length > 0) {
145
188
  logStep('düzeltme', `gateway ${fixes.length} otomatik düzeltme uyguladı — gövdeye istek gönderilmeden ÖNCE işlendi, yanıta gatewayNormalized olarak eklenir`, fixes);
@@ -44,6 +44,9 @@ export function microserviceOutline(definition) {
44
44
  url: definition.url,
45
45
  workflowId: definition.workflowId ?? null,
46
46
  queryId: definition.queryId ?? null,
47
+ // S-2: rol listesi okuma şeklinde `roles`, yazma gövdesinde `roleIds` — outline yazma adını
48
+ // basar ki patch'ten sonra "rol tuttu mu" sorusu ikinci bir microservice_get istemesin.
49
+ roleIds: asArray(definition.roles).map(role => role.id),
47
50
  sourceParameters: {
48
51
  operation: sourceParameters.operation,
49
52
  entityId: sourceParameters.entityId,
@@ -55,9 +58,10 @@ export function microserviceOutline(definition) {
55
58
  filterCount: asArray(sourceParameters.filters).length
56
59
  },
57
60
  view: 'outline',
58
- hint: "Parça için view:'part', part:'sourceParameters.joins' gibi; değişiklik için microservice_patch."
61
+ hint: "Parça için view:'part', part:'sourceParameters.joins' gibi; değişiklik için microservice_patch. Rol listesi patch belgesinde '/roles' [{ id, name }] yolundadır."
59
62
  };
60
63
  }
64
+ const enumOutlineValueLimit = 30;
61
65
  export function entityOutline(definition) {
62
66
  return {
63
67
  id: definition.id,
@@ -75,7 +79,16 @@ export function entityOutline(definition) {
75
79
  nullable: field.nullable,
76
80
  relationType: field.relationType ?? 'none',
77
81
  relationId: field.relationId ?? null,
78
- isLocalized: Boolean(field.isLocalized)
82
+ isLocalized: Boolean(field.isLocalized),
83
+ // S-1: enum'a tek op'la yazılan değerin kanıtı outline'da dursun — aksi hâlde `entity_patch`
84
+ // yanıtı "liste gitti mi" sorusuna cevap vermiyor ve ajan tam gövdeye dönüyor. Bütçe K3:
85
+ // ilk 30 değer; `icon`/`labels` outline'a girmez, sayı toplamı söyler.
86
+ ...(asArray(field.valueOptions).length > 0
87
+ ? {
88
+ values: asArray(field.valueOptions).slice(0, enumOutlineValueLimit).map(option => option.value),
89
+ valueOptionCount: asArray(field.valueOptions).length
90
+ }
91
+ : {})
79
92
  })),
80
93
  indexCount: asArray(definition.indexes).length,
81
94
  // Index'ler alanlardan çok daha az sayıdadır; tam projeksiyon outline'ı şişirmez ve yazılan
@@ -91,7 +104,7 @@ export function entityOutline(definition) {
91
104
  }
92
105
  : {}),
93
106
  view: 'outline',
94
- hint: "Bir alanın tam gövdesi için view:'part', part:'fields[slug=<slug>]'; değişiklik için entity_patch. isLocalized alanlar için platform_guide('entity') §'Çoklu dilli alan'."
107
+ hint: "Bir alanın tam gövdesi için view:'part', part:'fields[slug=<slug>]'; değişiklik için entity_patch. Enum'a tek değer: entity_patch add '/fields[slug=<slug>]/valueOptions/-'. isLocalized alanlar için platform_guide('entity') §'Çoklu dilli alan'."
95
108
  };
96
109
  }
97
110
  const identityKeys = ['id', 'slug', 'name', 'elementId', 'stepId'];
@@ -47,6 +47,47 @@ function hasText(value) {
47
47
  function stepLabel(step) {
48
48
  return step.name || step.type || step.id || '?';
49
49
  }
50
+ const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
51
+ /**
52
+ * `workflow` adımının (ve `loop` içindeki `stepType: 'workflow'` çocuğunun) alt akışı çağıran
53
+ * kuralı. `WorkflowStep.cs` yalnız `rule.WorkflowId` + `rule.Expression` okur; girdiyi başka
54
+ * bir alanla göndermeye çalışan gövde ya kaydedilip okunmaz (şema üyeleri) ya da hiç
55
+ * kaydedilmez (şema dışı adlar, `unmappedMembers`) — iki durumda da alt akış SESSİZCE girdisiz
56
+ * koşar (RFQ koşusu W-7, 2026-09-14). Platform her adım kuralını `fields: []`,
57
+ * `customParameters: null` gibi boş varsayılanlarla geri okuttuğu için yalnız DOLU değer sayılır;
58
+ * aksi hâlde `workflow_get` gövdesiyle yapılan her `workflow_patch`/`update`/`preview` kendi
59
+ * kendine bloklanırdı.
60
+ */
61
+ const WORKFLOW_STEP_SAVED_BUT_UNREAD_KEYS = ['payloadExpression', 'customParameters', 'queryParameters', 'bodyExpression', 'fields'];
62
+ const WORKFLOW_STEP_UNMAPPED_INPUT_KEYS = ['parameters', 'parameter', 'input', 'payload', 'arguments', 'args', 'variables'];
63
+ function carriesValue(value) {
64
+ if (value === undefined || value === null)
65
+ return false;
66
+ if (Array.isArray(value))
67
+ return value.length > 0;
68
+ if (typeof value === 'string')
69
+ return value.trim() !== '';
70
+ return true;
71
+ }
72
+ function workflowCallSites(body) {
73
+ return workflowSteps(body).flatMap((step) => {
74
+ const rule = (step.rule ?? {});
75
+ if (step.type === 'workflow')
76
+ return [{ step, rule, label: stepLabel(step) }];
77
+ if (step.type === 'loop' && rule.stepType === 'workflow' && rule.stepRule && typeof rule.stepRule === 'object') {
78
+ return [{ step, rule: rule.stepRule, label: `${stepLabel(step)} (loop çocuğu)` }];
79
+ }
80
+ return [];
81
+ });
82
+ }
83
+ function literalShapeOf(expression) {
84
+ const head = expression.trim().replace(/^[(\s]+/, '');
85
+ if (head.startsWith('{'))
86
+ return 'object';
87
+ if (head.startsWith('['))
88
+ return 'array';
89
+ return undefined;
90
+ }
50
91
  /**
51
92
  * Start'tan başlayıp `global-error` dalları HARİÇ tüm dalları izleyerek erişilen adımlar.
52
93
  * Gateway her `workflow`/`middleware` tanımına global-error dalının ucunda bir `response` adımı
@@ -698,6 +739,45 @@ export const policyRules = [
698
739
  });
699
740
  }
700
741
  },
742
+ {
743
+ id: 'WF-032',
744
+ module: 'workflow',
745
+ appliesTo: ['create', 'update', 'preview'],
746
+ sourceRef: "WorkflowStep.cs EvaluateAsync: kural yalnız rule.WorkflowId ve rule.Expression okur; girdi workflow.HasParameter iken Expression'ın DataTransferObjectExpression.Eval'iyle bağlanır, başka hiçbir alan okunmaz. Studio WorkflowTrigger.svelte de yalnız bu iki alanı yazar ve expression'ı çağrılan akışın parameterScheme'inden nesne literali olarak doldurur. RFQ koşusu W-7 (2026-09-14): payloadExpression/customParameters kaydedildi, alt akış girdisiz koştu",
747
+ check: (definition, context) => workflowCallSites(workflowBody(definition, context.workflowKind)).flatMap(({ label, rule }) => {
748
+ const workflowId = rule.workflowId;
749
+ if (!hasText(workflowId)) {
750
+ return [{
751
+ ruleId: 'WF-032',
752
+ severity: 'block',
753
+ message: `'${label}' workflow adımında rule.workflowId yok — adım hangi akışı çağıracağını bilmez. workflow_list(applicationId, type: 'workflow') ile id seçin; girdi rule.expression'a nesne literali olarak yazılır (çağrılan akışın parametre DTO'suna göre).`,
754
+ guide: 'ngapps://guides/workflow'
755
+ }];
756
+ }
757
+ if (!GUID_PATTERN.test(String(workflowId))) {
758
+ return [{
759
+ ruleId: 'WF-032',
760
+ severity: 'block',
761
+ message: `'${label}' adımının rule.workflowId'si GUID değil: '${String(workflowId)}'. Platform alanı Guid'e çeviremediği için 400 döner; slug değil id verin (workflow_list).`,
762
+ guide: 'ngapps://guides/workflow'
763
+ }];
764
+ }
765
+ const savedButUnread = WORKFLOW_STEP_SAVED_BUT_UNREAD_KEYS.filter(key => carriesValue(rule[key]));
766
+ const unmapped = WORKFLOW_STEP_UNMAPPED_INPUT_KEYS.filter(key => carriesValue(rule[key]));
767
+ if (savedButUnread.length === 0 && unmapped.length === 0)
768
+ return [];
769
+ const fate = [
770
+ savedButUnread.length > 0 ? `${savedButUnread.map(key => `rule.${key}`).join(', ')} kaydedilir ama WorkflowStep okumaz` : '',
771
+ unmapped.length > 0 ? `${unmapped.map(key => `rule.${key}`).join(', ')} StepRuleDto'da yok, platform hiç kaydetmez (unmappedMembers)` : ''
772
+ ].filter(part => part !== '').join('; ');
773
+ return [{
774
+ ruleId: 'WF-032',
775
+ severity: 'block',
776
+ message: `'${label}' adımı alt akışa girdiyi yanlış alandan gönderiyor — ${fate}. WorkflowStep yalnız rule.workflowId ve rule.expression okur: iki durumda da alt akış boş girdiyle koşar ve 200 döner (RFQ koşusu W-7). Eşlemeyi rule.expression'a nesne literali olarak yazın — her anahtar çağrılan akışın parametre DTO'sundaki bir alan, her değer bir JS ifadesi: "({ title: input.title, source: 'telemetry' })". Aynı adlı alan code adımında GÖVDE, workflow/microfunction adımında GİRDİDİR. Alan listesini workflow_step_schema (parameterScheme) ya da workflow_get(<workflowId>).parameterDtoId → dto_get verir.`,
777
+ guide: 'ngapps://guides/workflow'
778
+ }];
779
+ })
780
+ },
701
781
  {
702
782
  id: 'WF-006',
703
783
  module: 'workflow',
@@ -2092,7 +2172,7 @@ export const policyRules = [
2092
2172
  id: 'WF-018',
2093
2173
  module: 'workflow',
2094
2174
  appliesTo: ['create', 'update', 'preview'],
2095
- sourceRef: "LoopStep.cs:41 çocuk kuralı rule.Children.FirstOrDefault() ile okur ama API sözleşmesinde alan TEKİLdir (StepRuleDto.StepRule, WorkflowDto.cs:498). 'children' gönderilirse Children boş kalır, çocuk adım null rule ile koşar ve loop NullReferenceException verir — hata arrayExpression'ı işaret ettiği için yanlış yere baktırır. Ölçüm: anket-admin/anket-kaydet, 2026-08-19",
2175
+ sourceRef: "LoopStep.cs:41 çocuk kuralı rule.Children.FirstOrDefault() ile okur ama API sözleşmesinde alan TEKİLdir (StepRuleDto.StepRule, WorkflowDto.cs:498). 'children' gönderilirse Children boş kalır, çocuk adım null rule ile koşar ve loop NullReferenceException verir — hata arrayExpression'ı işaret ettiği için yanlış yere baktırır. Ölçüm: anket-admin/anket-kaydet, 2026-08-19. loopItem kapsamı 2026-09-16'da ölçüldü: table çocuğunun filtre ifadesinde çalışıyor, code çocuğunda tanımsız (LoopStep.cs:38 loop'un kendi motoruna yazıyor, code adımı ayrı motor kuruyor)",
2096
2176
  check: (definition, context) => {
2097
2177
  const body = workflowBody(definition, context.workflowKind);
2098
2178
  return workflowSteps(body)
@@ -2105,7 +2185,7 @@ export const policyRules = [
2105
2185
  findings.push({
2106
2186
  ruleId: 'WF-018',
2107
2187
  severity: 'block',
2108
- message: `'${stepLabel(step)}' loop step'inde rule.stepRule yok${hasChildren ? " — ama rule.children dolu: çocuk kural alanının adı 'children' DEĞİL 'stepRule'dur (tekil)" : ''}. Çocuksuz loop çalışma anında NullReferenceException verir ve hata mesajı arrayExpression'ı suçlar. Çocuğun kendi rule gövdesini rule.stepRule'a koyun; geçerli öğeye çocuk ifadelerinde 'loopItem' ile erişilir, loop'un çıktısı [{ item, result }] dizisidir.`,
2188
+ message: `'${stepLabel(step)}' loop step'inde rule.stepRule yok${hasChildren ? " — ama rule.children dolu: çocuk kural alanının adı 'children' DEĞİL 'stepRule'dur (tekil)" : ''}. Çocuksuz loop çalışma anında NullReferenceException verir ve hata mesajı arrayExpression'ı suçlar. Çocuğun kendi rule gövdesini rule.stepRule'a koyun; loop'un çıktısı [{ item, result }] dizisidir. Geçerli öğeye 'loopItem' ile erişilir AMA yalnız loop'un kendi değerlendirdiği ifadelerde (table çocuğunun fields[].value / filters[].value gibi) — 'code' çocuğunda loopItem TANIMSIZDIR ve adım "loopItem is not defined" ile düşer (ölçüldü 2026-09-16); her öğe için hesabı arrayExpression'da önden yapın.`,
2109
2189
  guide: 'ngapps://guides/workflow'
2110
2190
  });
2111
2191
  }
@@ -2765,6 +2845,74 @@ export const policyRules = [
2765
2845
  guide: 'ngapps://guides/entity'
2766
2846
  }))
2767
2847
  },
2848
+ /**
2849
+ * WF-033 — `http` adımının sözleşmesi. Gövde ayrı bir alan değildir: `payloadExpression` tek
2850
+ * ifadeyle `{ header, body, queryString }` ÜÇÜNÜ birden üretmek zorundadır, çünkü yürütücü
2851
+ * üçüne de `dynamic` üye erişimiyle bakar (HttpStep.cs:32,42,59) ve eksik anahtar adımı
2852
+ * `RuntimeBinderException` ile düşürür — ölçüldü 2026-09-16, mesaj birebir
2853
+ * "'System.Dynamic.ExpandoObject' does not contain a definition for 'queryString'".
2854
+ *
2855
+ * Anahtar denetimi YALNIZ nesne literalinde yapılır (`(`/`{` ile başlayan ifade): bir fonksiyon
2856
+ * çağrısı ya da değişken döndüren ifadenin içini statik olarak göremeyiz ve orada susmak
2857
+ * yanlış pozitif üretmemekten iyidir (AGENTS §10).
2858
+ */
2859
+ {
2860
+ id: 'WF-033',
2861
+ module: 'workflow',
2862
+ appliesTo: ['create', 'update', 'preview'],
2863
+ sourceRef: "HttpStep.cs:26-32,42,59,63-85 — url rule.UrlExpression'dan (JS ifadesi → string), method rule.HttpMethod'dan (HttpMethod.Parse), gövde rule.PayloadExpression'ın body/header/queryString anahtarlarından okunur; eksik anahtar RuntimeBinderException. contentType boşsa gövde body.ToString() ile gider. Ölçüm 2026-09-16, test/development: üç anahtarlı çağrı 200, queryString'siz çağrı RuntimeBinderException",
2864
+ check: definition => {
2865
+ const httpMethods = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options']);
2866
+ const bodyCarryingMethods = new Set(['post', 'put', 'patch']);
2867
+ return workflowSteps(workflowBody(definition))
2868
+ .filter(step => step.type === 'http')
2869
+ .flatMap((step) => {
2870
+ const rule = (step.rule ?? {});
2871
+ const findings = [];
2872
+ const payload = String(rule.payloadExpression ?? '').trim();
2873
+ if (!hasText(rule.urlExpression)) {
2874
+ findings.push({
2875
+ ruleId: 'WF-033',
2876
+ severity: 'block',
2877
+ message: `'${stepLabel(step)}' http adımında rule.urlExpression yok. Adres bir JS İFADESİDİR, düz metin değil: sabit adresi tırnak içinde yazın ("'https://api.example.com/v1/rates'"), dinamik adresi ifadeyle kurun ("'https://api.example.com/orders/' + input.orderId"). Tırnaksız yazarsanız JS onu değişken sanır ve adım "x is not defined" ile düşer.`,
2878
+ guide: 'ngapps://guides/workflow'
2879
+ });
2880
+ }
2881
+ if (!httpMethods.has(String(rule.httpMethod ?? '').trim().toLowerCase())) {
2882
+ findings.push({
2883
+ ruleId: 'WF-033',
2884
+ severity: 'block',
2885
+ message: `'${stepLabel(step)}' http adımında rule.httpMethod ${hasText(rule.httpMethod) ? `'${String(rule.httpMethod)}' — tanınmıyor` : 'yok'}. Platform değeri HttpMethod.Parse ile çözer; geçerli değerler: ${[...httpMethods].map(method => method.toUpperCase()).join(' · ')}.`,
2886
+ guide: 'ngapps://guides/workflow'
2887
+ });
2888
+ }
2889
+ if (payload.startsWith('(') || payload.startsWith('{')) {
2890
+ const missing = ['body', 'header', 'queryString']
2891
+ .filter(key => !new RegExp(`['"]?${key}['"]?\\s*:`).test(payload));
2892
+ if (missing.length > 0) {
2893
+ findings.push({
2894
+ ruleId: 'WF-033',
2895
+ severity: 'block',
2896
+ message: `'${stepLabel(step)}' http adımının payloadExpression'ında ${missing.join(', ')} anahtarı yok. Gövde AYRI BİR ALAN DEĞİLDİR: bu tek ifade body, header ve queryString ÜÇÜNÜ BİRDEN taşımak zorundadır — boş olanı da yazın: "({ body: {}, header: {}, queryString: {} })". Eksik anahtar çalışma anında RuntimeBinderException verir ("'System.Dynamic.ExpandoObject' does not contain a definition for '${missing[0]}'") ve adım fail dalına düşer. Örnek: ngapps://examples/workflow-http-step`,
2897
+ guide: 'ngapps://guides/workflow'
2898
+ });
2899
+ }
2900
+ }
2901
+ // Gövde taşıyan metotlarda contentType'ın boş olması sessiz bir bozulmadır. Gövdenin
2902
+ // DOLU olup olmadığını ifade metninden ayrıştırmaya çalışmıyoruz (K7): metot yeterli
2903
+ // ve kesin bir sinyal, gövde ayrıştırması yanlış pozitif üretir.
2904
+ if (!hasText(rule.contentType) && bodyCarryingMethods.has(String(rule.httpMethod ?? '').trim().toLowerCase())) {
2905
+ findings.push({
2906
+ ruleId: 'WF-033',
2907
+ severity: 'warn',
2908
+ message: `'${stepLabel(step)}' http adımı ${String(rule.httpMethod).toUpperCase()} ile gövde gönderiyor ama rule.contentType boş: platform gövdeyi body.ToString() ile yollar ve nesne gövdesi tip adının kendisine dönüşür (HttpStep.cs:63-85). JSON gönderiyorsanız contentType: 'application/json' yazın.`,
2909
+ guide: 'ngapps://guides/workflow'
2910
+ });
2911
+ }
2912
+ return findings;
2913
+ });
2914
+ }
2915
+ },
2768
2916
  {
2769
2917
  id: 'MW-003',
2770
2918
  module: 'workflow',
@@ -2873,6 +3021,89 @@ export async function microfunctionStepFindings(platform, project, environment,
2873
3021
  }
2874
3022
  return applyPolicyMode(platform.config, findings, policyOverride);
2875
3023
  }
3024
+ /**
3025
+ * WF-032'nin ikinci yarısı. `rule.expression`'ın işe yarayıp yaramayacağı gövdeden görülemez:
3026
+ * `WorkflowStep` ifadeyi YALNIZ çağrılan akışın parametre DTO'su varken değerlendirir
3027
+ * (`workflow.HasParameter`); DTO'suz alt akışa yazılan ifade hiç koşmaz ve girdi boş gider.
3028
+ * Karar çağrılan akışın kaydında yaşar — gateway `workflow_get` ucunu okur ve bulguyu aynı
3029
+ * `policyFindings` kanalına koyar; `policyOverride` / `POLICY=warn` kaçışları aynen geçerlidir.
3030
+ *
3031
+ * Okuma başarısız olursa kural SUSAR (WF-MF-001 ile aynı gerekçe). Uygulama kimliği ne
3032
+ * parametrede ne gövdede varsa (automation yolu `entityId` ile çalışır) da susar: statik yarı
3033
+ * yine koşar, canlı yarı bir kolaylıktır.
3034
+ */
3035
+ export async function workflowStepFindings(platform, project, environment, applicationId, definition, kind, policyOverride = false) {
3036
+ if (platform.config.policyMode === 'off')
3037
+ return [];
3038
+ const application = hasText(applicationId)
3039
+ ? String(applicationId)
3040
+ : hasText(definition.applicationId) ? String(definition.applicationId) : undefined;
3041
+ if (application === undefined)
3042
+ return [];
3043
+ const sites = workflowCallSites(workflowBody(definition, kind))
3044
+ .filter(site => GUID_PATTERN.test(String(site.rule.workflowId ?? '')));
3045
+ if (sites.length === 0)
3046
+ return [];
3047
+ const guide = 'ngapps://guides/workflow';
3048
+ const records = new Map();
3049
+ const findings = [];
3050
+ for (const { label, rule } of sites) {
3051
+ const workflowId = String(rule.workflowId);
3052
+ if (!records.has(workflowId)) {
3053
+ try {
3054
+ const payload = await platform.tenant('GET', project, `/workflows/${application}/${workflowId}`, { environment });
3055
+ records.set(workflowId, (payload?.data ?? payload));
3056
+ }
3057
+ catch (error) {
3058
+ records.set(workflowId, error instanceof PlatformApiError && error.status === 404 ? 'missing' : 'unreadable');
3059
+ }
3060
+ }
3061
+ const record = records.get(workflowId);
3062
+ if (record === undefined || record === 'unreadable')
3063
+ continue;
3064
+ if (record === 'missing') {
3065
+ findings.push({
3066
+ ruleId: 'WF-032',
3067
+ severity: 'block',
3068
+ message: `'${label}' adımının workflowId'si (${workflowId}) bu ortamda YOK (platform kaydı yalnız id ile arar, uygulama kimliğine bakmaz). Adım kuralı step_rules.workflow_id ile kayda FK'lıdır: gövde gitseydi platform 400 dönerdi. workflow_list(applicationId: '${application}', type: 'workflow') ile bu ortamdaki akışları görün — alt akış başka bir dalda ya da ortamda kalmış, ya da henüz yaratılmamış olabilir.`,
3069
+ guide
3070
+ });
3071
+ continue;
3072
+ }
3073
+ const calleeName = String(record.name ?? record.slug ?? workflowId);
3074
+ const parameterDtoId = hasText(record.parameterDtoId) ? String(record.parameterDtoId) : undefined;
3075
+ const expression = hasText(rule.expression) ? String(rule.expression) : '';
3076
+ if (expression !== '' && parameterDtoId === undefined) {
3077
+ findings.push({
3078
+ ruleId: 'WF-032',
3079
+ severity: 'block',
3080
+ message: `'${label}' adımı '${calleeName}' akışına rule.expression ile girdi geçiriyor ama o akışın parametre DTO'su YOK (parameterDtoId boş). WorkflowStep ifadeyi yalnız workflow.HasParameter iken değerlendirir: bu hâliyle ifade HİÇ çalışmaz, alt akış boş girdiyle koşar ve 200 döner. Önce alt akışa girdi sözleşmesi verin — dto_create ile alan listesini tanımlayın, sonra workflow_patch(id: '${workflowId}', ops: [{ op: 'add', path: '/parameterDtoId', value: '<dtoId>' }, { op: 'add', path: '/parameterBehavior', value: 'object' }]); alt akışın adımları girdiyi input.<alan> ile okur.`,
3081
+ guide
3082
+ });
3083
+ continue;
3084
+ }
3085
+ if (expression === '' && parameterDtoId !== undefined) {
3086
+ findings.push({
3087
+ ruleId: 'WF-032',
3088
+ severity: 'warn',
3089
+ message: `'${label}' adımında rule.expression boş ama '${calleeName}' akışının parametre DTO'su var (parameterDtoId: ${parameterDtoId}). Alt akış input olarak boş nesne alır, kendi input.<alan> okumaları undefined döner. Girdiyi nesne literaliyle geçirin — alan listesini workflow_step_schema çıktısındaki parameterScheme ya da dto_get('${parameterDtoId}') verir.`,
3090
+ guide
3091
+ });
3092
+ continue;
3093
+ }
3094
+ const behavior = String(record.parameterBehavior ?? '').toLowerCase();
3095
+ const literal = literalShapeOf(expression);
3096
+ if (literal !== undefined && ((behavior === 'array' && literal === 'object') || (behavior === 'object' && literal === 'array'))) {
3097
+ findings.push({
3098
+ ruleId: 'WF-032',
3099
+ severity: 'warn',
3100
+ message: `'${label}' adımının ifadesi ${literal === 'object' ? 'nesne' : 'dizi'} literaliyle başlıyor ama '${calleeName}' akışının parameterBehavior'ı '${behavior}': alt akış input olarak ${behavior === 'array' ? 'DİZİ bekler; nesne alınca öğe okumaları' : 'NESNE bekler; dizi alınca input.<alan> okumaları'} undefined döner. İfadeyi davranışa uydurun ya da alt akışın parameterBehavior'ını değiştirin.`,
3101
+ guide
3102
+ });
3103
+ }
3104
+ }
3105
+ return applyPolicyMode(platform.config, findings, policyOverride);
3106
+ }
2876
3107
  const partialWriteOperations = new Set(['create', 'update', 'upsert']);
2877
3108
  const inputReferencePattern = /\binput\.([A-Za-z_$][\w$]*)/g;
2878
3109
  function inputReferencesOf(rule) {
@@ -177,6 +177,79 @@ function diagnoseKnownFailure(status, message, code, context) {
177
177
  }
178
178
  return '';
179
179
  }
180
+ const MaxErrorCauseWalk = 32;
181
+ const MaxErrorPathShown = 6;
182
+ const MaxErrorTextLength = 400;
183
+ function nonEmptyText(value) {
184
+ if (typeof value !== 'string' || value.trim() === '')
185
+ return undefined;
186
+ return value.length > MaxErrorTextLength ? `${value.slice(0, MaxErrorTextLength)}…` : value;
187
+ }
188
+ function asRecord(value) {
189
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
190
+ }
191
+ function errorLevels(body) {
192
+ const levels = [];
193
+ let current = asRecord(body);
194
+ while (current) {
195
+ if (levels.length === MaxErrorCauseWalk)
196
+ return { levels, truncated: true };
197
+ const step = asRecord(current.step);
198
+ const name = nonEmptyText(step?.name);
199
+ const stepType = nonEmptyText(step?.type);
200
+ levels.push({
201
+ step: name ? `'${name}'${stepType ? ` (${stepType})` : ''}` : undefined,
202
+ code: nonEmptyText(current.code),
203
+ message: nonEmptyText(current.message),
204
+ detail: nonEmptyText(current.detail),
205
+ type: nonEmptyText(current.type)
206
+ });
207
+ current = asRecord(current.cause);
208
+ }
209
+ return { levels, truncated: false };
210
+ }
211
+ function seenKey(...parts) {
212
+ return JSON.stringify(parts);
213
+ }
214
+ /**
215
+ * Workflow 5xx gövdesinden, başlıkta zaten yazılmış `code`/`message` dışında kalan bilgiyi çıkarır.
216
+ * İki gövde şekli okunur: platformun `WorkflowStepErrorResponse`'u `{ code, message, detail, type }`
217
+ * (hata yolu olmayan akış, `ExceptionHandler.cs`) ve gateway hata adımının gövdesi
218
+ * `{ …, step: { name, type }, cause }` (`errorPath.ts`). `cause` zinciri alt akış seviyelerini
219
+ * taşır; aynı kod+mesaj her seviyede tekrarlandığı için mesaj EN DERİNDEN başlanarak bir kez, hatanın
220
+ * doğduğu adıma atfedilerek yazılır. Kodla aynı `type` ve mesajla aynı `detail` gürültüdür. Uzun
221
+ * zincirde yolun başı ve SONU gösterilir (hatanın doğduğu adım kaybolmasın); metinler kırpılır.
222
+ */
223
+ export function describeErrorBody(body, shown) {
224
+ const { levels, truncated } = errorLevels(body);
225
+ const fragments = [];
226
+ const path = levels.map(level => level.step).filter((step) => step !== undefined);
227
+ const shownPath = path.length > MaxErrorPathShown ? [...path.slice(0, 2), '…', ...path.slice(-3)] : path;
228
+ if (shownPath.length > 0)
229
+ fragments.push(`hata yolu: ${shownPath.join(' → ')}${truncated ? ' → …' : ''}`);
230
+ const seen = new Set();
231
+ if (shown) {
232
+ seen.add(seenKey('head', shown.code, shown.message));
233
+ if (levels[0])
234
+ seen.add(seenKey('head', levels[0].code, levels[0].message));
235
+ }
236
+ for (const level of [...levels].reverse()) {
237
+ const key = seenKey('head', level.code, level.message);
238
+ if ((level.code || level.message) && !seen.has(key)) {
239
+ seen.add(key);
240
+ fragments.push(`${level.step ? `${level.step} ` : ''}${[level.code, level.message].filter(Boolean).join(': ')}`);
241
+ }
242
+ if (level.detail && level.detail !== level.message && !seen.has(seenKey('detail', level.detail))) {
243
+ seen.add(seenKey('detail', level.detail));
244
+ fragments.push(`detail: ${level.detail}`);
245
+ }
246
+ if (level.type && level.type !== level.code && !seen.has(seenKey('type', level.type))) {
247
+ seen.add(seenKey('type', level.type));
248
+ fragments.push(`type: ${level.type}`);
249
+ }
250
+ }
251
+ return fragments;
252
+ }
180
253
  export function mapPlatformError(status, payload, context) {
181
254
  const body = payload && typeof payload === 'object' ? payload : {};
182
255
  const code = typeof body.code === 'string' ? body.code : undefined;
@@ -222,7 +295,9 @@ export function mapPlatformError(status, payload, context) {
222
295
  }
223
296
  const fallbackValidationMessages = collectValidationMessages(body);
224
297
  const fallbackDetail = fallbackValidationMessages.length > 0 ? ` → ${fallbackValidationMessages.join(' ; ')}` : '';
225
- return new PlatformApiError(`Platform hatası ${status} (${location}): ${apiMessage ?? 'beklenmeyen hata'}${code ? ` [${code}]` : ''}${fallbackDetail}${diagnoseKnownFailure(status, apiMessage ?? '', code, context)}`, status, code);
298
+ const errorBody = status >= 500 ? describeErrorBody(body, { code, message: apiMessage }) : [];
299
+ const errorBodyDetail = errorBody.length > 0 ? ` — ${errorBody.join(' · ')}` : '';
300
+ return new PlatformApiError(`Platform hatası ${status} (${location}): ${apiMessage ?? 'beklenmeyen hata'}${code ? ` [${code}]` : ''}${errorBodyDetail}${fallbackDetail}${diagnoseKnownFailure(status, apiMessage ?? '', code, context)}`, status, code);
226
301
  }
227
302
  export function mapNetworkError(error, context) {
228
303
  const cause = error instanceof Error ? error : new Error(String(error));
@@ -1,5 +1,5 @@
1
1
  import { logError, logStep } from '../observability/trace.js';
2
- import { PlatformApiError } from './errors.js';
2
+ import { PlatformApiError, describeErrorBody } from './errors.js';
3
3
  import { PolicyBlockError } from '../knowledge/policy.js';
4
4
  import { maskSecrets } from './secrets.js';
5
5
  export function prune(value) {
@@ -76,6 +76,17 @@ export function runtimeErrorSummary(payload) {
76
76
  const errors = [];
77
77
  if (typeof record.httpStatus === 'number' && record.httpStatus >= 400) {
78
78
  errors.push(`httpStatus: ${record.httpStatus} — çağrı ${record.httpStatus} ile bitti; gövde başarı çıktısı değil hata gövdesidir. Adım adım bakmak için microservice_debug kullanın.`);
79
+ // Yalnız 5xx: 4xx gövdeleri (422 doğrulama, 401) bilinçli iş yanıtıdır ve zaten gövdede okunur.
80
+ // `/debug/` ucu gövdeyi her zaman, `/m/` ve servis önizlemesi ise uiActions kurulmuşsa
81
+ // `{ data, uiActions[, executionItems] }` ile sarar (MicroservicesController.Debug / HandleResult).
82
+ const raw = record.body && typeof record.body === 'object' && !Array.isArray(record.body) ? record.body : undefined;
83
+ const wrapped = raw !== undefined && ('executionItems' in raw || 'uiActions' in raw) && raw.data && typeof raw.data === 'object' && !Array.isArray(raw.data);
84
+ const errorBody = wrapped ? raw.data : raw;
85
+ const describable = record.httpStatus >= 500 && errorBody !== undefined &&
86
+ (typeof errorBody.code === 'string' || Boolean(errorBody.step && typeof errorBody.step === 'object') || Boolean(errorBody.cause && typeof errorBody.cause === 'object'));
87
+ const fragments = describable ? describeErrorBody(errorBody) : [];
88
+ if (fragments.length > 0)
89
+ errors.push(`hata gövdesi — ${fragments.join(' · ')}`);
79
90
  }
80
91
  const stepErrors = [];
81
92
  collectStepErrors(body.steps, stepErrors);
@@ -10,7 +10,8 @@
10
10
  "scope": "Yalnız source:'studio'. application: her sayfada (tarayıcı). page: yalnız o sayfanın tarayıcı bağlamında, pageId zorunlu. Ad uygulama içinde kapsamlar arası benzersizdir (K3); workflow adı tenant içinde benzersizdir. Aynı ad iki kaynakta yan yana olabilir.",
11
11
  "isAsync": "Yalnız studio'da anlamlı; false bırakın: true olsaydı {{ }} bağlaması '[object Promise]' çizerdi. await gerekmeyen gövdede isAsync:true FN-004 uyarısı alır. workflow kaynağında true FN-004 block (motor senkron).",
12
12
  "naming": "Ad JS tanımlayıcısı; ayrılmış sözcük olamaz. studio'da 'Functions' yasak (FN-001, block); workflow'da motor global adları (system, input, Now, crypto…) yasak (FN-001, block). Uygulama fonksiyonunda sayfaya özgü ad (komponent adı, state.page), workflow fonksiyonunda adım adı kullanmayın — çağrıldığı yerde yoksa 'X is not defined'.",
13
- "source": "workflow: SUNUCU, tenant geneli, applicationId/scope/pageId VERİLMEZ, isAsync olamaz, çıplak adla çağrılır. studio: TARAYICI, applicationId + scope zorunlu (application: her sayfada; page: yalnız o sayfa, pageId zorunlu), Functions.<ad>() ile çağrılır. Kaynak sonradan değişmez; sunucu ifadesinde Functions.<ad>( → WF-030 block."
13
+ "source": "workflow: SUNUCU, tenant geneli, applicationId/scope/pageId VERİLMEZ, isAsync olamaz, çıplak adla çağrılır. studio: TARAYICI, applicationId + scope zorunlu (application: her sayfada; page: yalnız o sayfa, pageId zorunlu), Functions.<ad>() ile çağrılır. Kaynak sonradan değişmez; sunucu ifadesinde Functions.<ad>( → WF-030 block.",
14
+ "update": "function_update TAM gövde PUT'lar ama gateway mevcut kaydı okuyup vermediğin alanları oradan doldurur: gövdeyi değiştirmek için { functionId, body } yeter — ayrı bir patch aracı YOKTUR, fonksiyonun tek büyük alanı gövdedir. source/applicationId/scope/pageId DEĞİŞMEZ. Yanıt dependents (kullanan flow/workflow adları) ve signatureChange (ad ya da parametre ad/sırası) taşır; ikisi de doluysa warnings çağıranların etkileneceğini söyler ve güncelleme YİNE yapılır (K12). Adı değiştirirsen platform çağıranları aynı işlemde yeni ada taşır ve renamedReferences kaç kaydın yazıldığını söyler."
14
15
  },
15
16
  "functionCreateArgs": {
16
17
  "source": "studio",
@@ -55,6 +56,10 @@
55
56
  ],
56
57
  "body": "return fiyat * adet;"
57
58
  },
59
+ "functionUpdateArgs": {
60
+ "functionId": "<function_list id>",
61
+ "body": "return fiyat * adet * 1.2;"
62
+ },
58
63
  "usage": {
59
64
  "propBinding": {
60
65
  "op": "set_binding",