@appaflytech/zeyrek-ai-mcp 0.0.2 → 0.0.4
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.
- package/.env.example +21 -7
- package/.env.production.example +42 -0
- package/README.md +14 -7
- package/dist/config.js +113 -46
- package/dist/knowledge/componentCatalog.js +476 -32
- package/dist/knowledge/componentEvents.js +154 -12
- package/dist/knowledge/errorPath.js +64 -0
- package/dist/knowledge/normalize.js +12 -1
- package/dist/knowledge/outlines.js +9 -2
- package/dist/knowledge/rules.js +184 -3
- package/dist/knowledge/styleResolve.js +485 -0
- package/dist/observability/trace.js +4 -0
- package/dist/platform/http.js +3 -0
- package/dist/platform/responses.js +80 -4
- package/dist/platform/screenshot.js +106 -0
- package/dist/resources/examples/layout-app-shell.json +368 -0
- package/dist/resources/examples/microservice-table-list.json +1 -0
- package/dist/resources/examples/middleware-authenticate.json +243 -47
- package/dist/resources/examples/workflow-basic.json +24 -2
- package/dist/resources/guides/component.md +6 -3
- package/dist/resources/guides/entity.md +33 -1
- package/dist/resources/guides/external-component.md +129 -0
- package/dist/resources/guides/layout.md +147 -0
- package/dist/resources/guides/microservice.md +35 -0
- package/dist/resources/guides/middleware.md +10 -0
- package/dist/resources/guides/page.md +137 -15
- package/dist/resources/guides/translation.md +80 -0
- package/dist/resources/guides/workflow.md +127 -2
- package/dist/server.js +4 -0
- package/dist/tools/externalComponents.js +285 -0
- package/dist/tools/layouts.js +48 -6
- package/dist/tools/microservices.js +2 -2
- package/dist/tools/pages.js +98 -30
- package/dist/tools/platformInfo.js +2 -2
- package/dist/tools/projects.js +7 -2
- package/dist/tools/screens.js +36 -0
- package/dist/tools/translations.js +37 -0
- package/dist/tools/workflows.js +3 -3
- package/package.json +4 -1
|
@@ -21,12 +21,53 @@ export const componentEventContracts = {
|
|
|
21
21
|
'qm-card': { click: NO_DATA },
|
|
22
22
|
form: { mount: NO_DATA, submit: NO_DATA },
|
|
23
23
|
container: { mount: NO_DATA },
|
|
24
|
+
aibox: { mount: NO_DATA },
|
|
25
|
+
box: { mount: NO_DATA },
|
|
26
|
+
'box-section': { mount: NO_DATA },
|
|
27
|
+
divider: {},
|
|
28
|
+
spacer: {},
|
|
29
|
+
popover: {
|
|
30
|
+
open: { payload: { isOpen: 'boolean (true)' }, reads: 'setter: self.isOpen da yazılır — akış paneli isOpen prop’unu değiştirerek de açıp kapatabilir' },
|
|
31
|
+
close: { payload: { isOpen: 'boolean (false)' }, reads: 'setter: self.isOpen da yazılır' }
|
|
32
|
+
},
|
|
33
|
+
phoneinput: {
|
|
34
|
+
mount: NO_DATA,
|
|
35
|
+
change: { payload: { value: 'string (E.164, ör. +905321234567)' }, reads: "setter: self.value da yazılır. DEĞER DEĞİŞMEDİYSE YAYILMAZ — hane sınırına çarpan tuşlar ve süzülen harfler akış tetiklemez (PhoneInput.svelte:66)" },
|
|
36
|
+
blur: { payload: { value: 'string (E.164)' }, reads: 'change’in aksine her odak kaybında yayılır, değer aynı olsa bile; setter DEĞİLDİR' }
|
|
37
|
+
},
|
|
38
|
+
filterbuilder: {
|
|
39
|
+
mount: NO_DATA,
|
|
40
|
+
change: {
|
|
41
|
+
payload: { value: 'FilterGroup (iç içe and/or kural ağacı)', jexl: 'string (aynı ağacın hazır jexl ifadesi)' },
|
|
42
|
+
reads: "setter yalnız `value`yi self’e yazar; `jexl` YALNIZ event.data’dadır (params) — sorguya bağlayacağın alan odur"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
comments: {
|
|
46
|
+
mount: NO_DATA,
|
|
47
|
+
submit: {
|
|
48
|
+
payload: { text: 'string', parentId: "string (yanıtsa üst yorumun id’si, kök yorumda '')", mentions: 'string[] (metinden çözülmüş @kişi listesi)', author: 'string (currentUser)' },
|
|
49
|
+
reads: 'Komponent yorumu ANINDA iyimser olarak listeye ekler (bekleyen katman); kalıcılık akışın işidir — rows tazelenmezse bekleyen satır bir sonraki içerik değişiminde kaybolur'
|
|
50
|
+
},
|
|
51
|
+
like: { payload: { id: 'string', liked: 'boolean (yeni durum)', author: 'string' }, reads: 'beğeni de iyimserdir; sayaç rows tazelenene kadar yerel' },
|
|
52
|
+
edit: { payload: { id: 'string', text: 'string (yeni metin)', author: 'string' } },
|
|
53
|
+
delete: { payload: { id: 'string', author: 'string' }, reads: 'satır listeden hemen çıkarılır; uca yazmak akışın işidir' }
|
|
54
|
+
},
|
|
24
55
|
'auth-layout': { mount: NO_DATA, primaryClick: NO_DATA, secondaryClick: NO_DATA },
|
|
25
56
|
layout: { mount: NO_DATA, logout: NO_DATA },
|
|
57
|
+
'layout-shell': {
|
|
58
|
+
mount: NO_DATA,
|
|
59
|
+
// LayoutShell.svelte toggle(): dispatch('emit', { name, data, params }) — `params` VERİLDİĞİ
|
|
60
|
+
// için bu olay istisnadır: sidebarOpen hem self.sidebarOpen'dan hem event.data'dan okunur.
|
|
61
|
+
toggle: { payload: { sidebarOpen: 'boolean (panelin YENİ durumu)' }, reads: 'self.sidebarOpen; params verildiği için event.data.sidebarOpen de dolar' }
|
|
62
|
+
},
|
|
63
|
+
'layout-region': { mount: NO_DATA },
|
|
64
|
+
navigations: { mount: NO_DATA },
|
|
26
65
|
textinput: { mount: NO_DATA, change: valueChange('string'), blur: NO_DATA },
|
|
27
66
|
textarea: { mount: NO_DATA, change: valueChange('string'), blur: NO_DATA },
|
|
28
67
|
password: { mount: NO_DATA, change: valueChange('string'), blur: NO_DATA },
|
|
29
|
-
|
|
68
|
+
// `change` ve `changed` AYNI anda yayılır (NumberInput.svelte:199-200) ve ikisi de setter'dır;
|
|
69
|
+
// `changed` eski akışlarla uyum için duruyor — yeni akışı `change`'e bağla, ikisine birden değil.
|
|
70
|
+
numberinput: { mount: NO_DATA, change: valueChange('number'), changed: valueChange('number'), blur: NO_DATA },
|
|
30
71
|
incrementalinput: { mount: NO_DATA, change: valueChange('string'), blur: NO_DATA },
|
|
31
72
|
texteditor: { onMount: NO_DATA, onChange: valueChange('string (HTML)') },
|
|
32
73
|
checkbox: { change: valueChange('boolean'), checked: valueChange('boolean'), unchecked: valueChange('boolean') },
|
|
@@ -70,8 +111,11 @@ export const componentEventContracts = {
|
|
|
70
111
|
video: { mount: NO_DATA, click: NO_DATA, play: NO_DATA, pause: NO_DATA, ended: NO_DATA },
|
|
71
112
|
// — 2026-08-25 eşleme turu: ng-apps-ui'da olay YAYAN ama burada kaydı olmayan komponentler.
|
|
72
113
|
// Yükler kaynaktaki dispatch('emit', { name, data }) / machine.onEmit(id, { event, data })
|
|
73
|
-
// çağrılarından birebir çıkarıldı. Olay yaymayan komponentler (
|
|
74
|
-
//
|
|
114
|
+
// çağrılarından birebir çıkarıldı. Olay yaymayan komponentler (breadcrumb · qrcode · slot · tags ·
|
|
115
|
+
// text) bilinçli olarak kayıtsızdır. `notification` bu listede
|
|
116
|
+
// YANLIŞLIKLA yer almıştı — Notification.svelte hem timeout'ta hem kapatma tıklamasında
|
|
117
|
+
// dispatch('emit', { name:'toggled', data:{isHidden:true}, setter:true }) çağırıyor (düzeltme
|
|
118
|
+
// 2026-08-25c, `docs/20-komponent-bilgi-katmani.md` §9).
|
|
75
119
|
autocomplete: {
|
|
76
120
|
mount: NO_DATA,
|
|
77
121
|
change: valueChange('string (YAZILAN metin — option değeri değil)'),
|
|
@@ -82,6 +126,8 @@ export const componentEventContracts = {
|
|
|
82
126
|
blur: NO_DATA
|
|
83
127
|
},
|
|
84
128
|
avatar: { click: NO_DATA },
|
|
129
|
+
badge: { click: NO_DATA },
|
|
130
|
+
icon: { click: NO_DATA },
|
|
85
131
|
'breadcrumb-title': {
|
|
86
132
|
mount: NO_DATA,
|
|
87
133
|
clickItem: { payload: { currentItem: '{ text, flow, isHidden } — tıklanan iz öğesi' } },
|
|
@@ -103,22 +149,46 @@ export const componentEventContracts = {
|
|
|
103
149
|
refresh: NO_DATA,
|
|
104
150
|
retry: NO_DATA
|
|
105
151
|
},
|
|
106
|
-
checkboxgroup: { mount: NO_DATA, change: valueChange('string[] (seçili değerler)') },
|
|
152
|
+
checkboxgroup: { mount: NO_DATA, change: valueChange('string[] (seçili değerler)'), focus: NO_DATA, blur: NO_DATA },
|
|
107
153
|
emptystate: { actionClick: { payload: { text: 'string (aksiyonun metni)', index: 'number' }, reads: "aksiyonun kendi flow'u zaten doğrudan çalışır" } },
|
|
108
|
-
iframe: { mount: NO_DATA },
|
|
154
|
+
iframe: { mount: NO_DATA, load: NO_DATA, error: NO_DATA },
|
|
155
|
+
jsonviewer: {
|
|
156
|
+
nodeClick: {
|
|
157
|
+
payload: {
|
|
158
|
+
path: "string (nokta/köşeli-parantez yolu — 'user.roles[0].name'; kökte boş)",
|
|
159
|
+
key: 'string (düğümün anahtarı; dizi öğesinde indeks)',
|
|
160
|
+
kind: "'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'",
|
|
161
|
+
value: 'düğümün ham değeri (object/array için alt ağacın tamamı)'
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
copy: {
|
|
165
|
+
payload: {
|
|
166
|
+
path: "string (kopyalanan düğümün yolu; araç çubuğundan TÜMÜNÜ kopyalamada boş + kind:'root')",
|
|
167
|
+
kind: "'root' | 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'",
|
|
168
|
+
value: 'kopyalanan değer (tümünü kopyalamada normalize edilmiş TÜM veri)'
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
109
172
|
'language-selector': { change: { payload: { value: 'string (dil kodu)' }, reads: 'runtime ayrıca machine.setLanguage(value) çağırır' } },
|
|
173
|
+
notification: { toggled: { payload: { isHidden: 'boolean' } } },
|
|
110
174
|
otpinput: {
|
|
111
175
|
mount: NO_DATA,
|
|
112
176
|
change: valueChange('string (birleşik kod)'),
|
|
113
|
-
complete: { payload: { value: 'string (tüm haneler dolduğunda)' } }
|
|
177
|
+
complete: { payload: { value: 'string (tüm haneler dolduğunda)' } },
|
|
178
|
+
resend: NO_DATA,
|
|
179
|
+
focus: NO_DATA,
|
|
180
|
+
blur: NO_DATA
|
|
114
181
|
},
|
|
115
182
|
pagination: { mount: NO_DATA, change: { payload: { offset: 'number', limit: 'number' }, reads: 'self.offset · self.limit (sorguyu bunlara bağla)' } },
|
|
116
|
-
|
|
183
|
+
progress: { complete: NO_DATA },
|
|
184
|
+
rating: { mount: NO_DATA, change: valueChange('number (0..max; allowHalf ile yarım)'), focus: NO_DATA, blur: NO_DATA },
|
|
117
185
|
result: { actionClick: { payload: { text: 'string (aksiyonun metni)', index: 'number' }, reads: "aksiyonun kendi flow'u zaten doğrudan çalışır" } },
|
|
118
186
|
slider: {
|
|
119
187
|
mount: NO_DATA,
|
|
120
188
|
change: valueChange('number (sürüklerken sürekli)'),
|
|
121
|
-
changed: valueChange('number (tutamak bırakıldığında — pahalı akışı buna bağla)')
|
|
189
|
+
changed: valueChange('number (tutamak bırakıldığında — pahalı akışı buna bağla)'),
|
|
190
|
+
focus: NO_DATA,
|
|
191
|
+
blur: NO_DATA
|
|
122
192
|
},
|
|
123
193
|
statcard: { mount: NO_DATA, click: NO_DATA, footerClick: { payload: {}, reads: "footerFlow zaten doğrudan çalışır; footerHref varsa tıklama ona gider" } },
|
|
124
194
|
switcher: { change: valueChange('boolean'), checked: valueChange('boolean'), unchecked: valueChange('boolean') },
|
|
@@ -127,7 +197,7 @@ export const componentEventContracts = {
|
|
|
127
197
|
clickItem: { payload: { currentItem: 'tıklanan olay öğesi', index: 'number' }, reads: "öğenin kendi flow'u da doğrudan çalışır" },
|
|
128
198
|
showMore: NO_DATA
|
|
129
199
|
},
|
|
130
|
-
timepicker: { mount: NO_DATA, change: valueChange("string 'HH:mm' (temizlenince '')") },
|
|
200
|
+
timepicker: { mount: NO_DATA, change: valueChange("string 'HH:mm' (temizlenince '')"), focus: NO_DATA, blur: NO_DATA },
|
|
131
201
|
toast: {
|
|
132
202
|
open: NO_DATA,
|
|
133
203
|
close: NO_DATA,
|
|
@@ -144,12 +214,84 @@ export const componentEventContracts = {
|
|
|
144
214
|
searchResultClick: { payload: {}, reads: "tıklanan kayıt yalnız emit params'ında (item) taşınır ve ifade kapsamına GİRMEZ — kayıt id'si gerekiyorsa aramayı kendi komponentinle kur" }
|
|
145
215
|
},
|
|
146
216
|
'qm-sidebar': { mount: NO_DATA, collapsed: { payload: {}, reads: "daralma durumu emit'in value alanındadır; motor yalnız data'yı prop'a yazdığı için akışa ULAŞMAZ" } },
|
|
147
|
-
'qm-content': { mount: NO_DATA }
|
|
217
|
+
'qm-content': { mount: NO_DATA },
|
|
218
|
+
// — 2026-08-27 eşleme turu: components-new kuşağı (docs/20-komponent-bilgi-katmani.md §10).
|
|
219
|
+
// Bu komponentler emit'e params DA geçirir, dolayısıyla akışta event.data DOLU gelir
|
|
220
|
+
// (eski kuşakta boştu). kanban ve eventcalendar üst düzey data'yı BİLİNÇLİ göndermez —
|
|
221
|
+
// yükleri self'e hiç yazılmaz, yalnız event.data ile okunur. detailview'ün TEK olayı mount'tur
|
|
222
|
+
// (tıklama yaymaz — tıklanabilirlik gerekiyorsa kapsayıcıya sarılır).
|
|
223
|
+
detailview: { mount: NO_DATA },
|
|
224
|
+
eventcalendar: {
|
|
225
|
+
mount: NO_DATA,
|
|
226
|
+
monthChange: {
|
|
227
|
+
payload: { month: 'number (1-TABANLI)', year: 'number' },
|
|
228
|
+
reads: "yük self'e YAZILMAZ — event.data ile oku; gezinme okları/Bugün ile her görünümde yayılır, ilk mount'ta yayılmaz"
|
|
229
|
+
},
|
|
230
|
+
viewChange: { payload: { view: "'month' | 'week' | 'day'" }, reads: "self'e yazılmaz — event.data ile oku" },
|
|
231
|
+
dateClick: {
|
|
232
|
+
payload: { date: "string 'yyyy-MM-dd'" },
|
|
233
|
+
reads: "yalnız AY görünümünde ve isCreatable KAPALIYKEN; self'e yazılmaz — event.data ile oku"
|
|
234
|
+
},
|
|
235
|
+
eventCreate: {
|
|
236
|
+
payload: { start: "string (ay tıklaması: 'yyyy-MM-dd' · saat slotu: ISO)", end: 'string (slotta start+60dk)', allDay: 'boolean' },
|
|
237
|
+
reads: "self'e yazılmaz — event.data ile oku"
|
|
238
|
+
},
|
|
239
|
+
eventClick: {
|
|
240
|
+
payload: { id: 'string', title: 'string', row: 'kaynak satırın TAMAMI' },
|
|
241
|
+
reads: "self'e yazılmaz — event.data ile oku"
|
|
242
|
+
},
|
|
243
|
+
eventMove: {
|
|
244
|
+
payload: { id: 'string', row: 'kaynak satırın TAMAMI', start: 'string (ISO)', end: 'string (ISO)', previousStart: 'string (ISO)', previousEnd: 'string (ISO)' },
|
|
245
|
+
reads: "self'e yazılmaz — event.data ile oku; akış kalıcılaştırıp rows'u tazelemezse taşınan etkinlik geri döner"
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
importwizard: {
|
|
249
|
+
mount: NO_DATA,
|
|
250
|
+
complete: {
|
|
251
|
+
payload: {
|
|
252
|
+
rows: 'Record<string,string>[] (TEMİZ satırlar; değerler hep string)',
|
|
253
|
+
errors: 'Array<{ rowIndex, field, message, raw }>',
|
|
254
|
+
fileName: 'string',
|
|
255
|
+
total: 'number (temiz satır + hata KAYDI sayısı)'
|
|
256
|
+
},
|
|
257
|
+
reads: "hem self.rows/self.errors/… hem event.data dolu gelir (params kuşağı); satırları uca yazmak complete akışının işidir"
|
|
258
|
+
},
|
|
259
|
+
cancel: NO_DATA
|
|
260
|
+
},
|
|
261
|
+
kanban: {
|
|
262
|
+
mount: NO_DATA,
|
|
263
|
+
cardMove: {
|
|
264
|
+
payload: { id: 'string', row: 'kaynak satırın TAMAMI', from: 'string (kaynak kolon value)', to: 'string (hedef kolon value)' },
|
|
265
|
+
reads: "BİLİNÇLİ olarak self'e yazılmaz — event.data ile oku; yeni kolonu uca yaz + rows'u tazele, yoksa kart geri döner"
|
|
266
|
+
},
|
|
267
|
+
cardClick: { payload: { id: 'string', row: 'kaynak satırın TAMAMI' }, reads: "self'e yazılmaz — event.data ile oku" }
|
|
268
|
+
},
|
|
269
|
+
signature: {
|
|
270
|
+
mount: NO_DATA,
|
|
271
|
+
change: {
|
|
272
|
+
payload: { value: "string (PNG data-URL ya da HAM SVG işaretlemesi; temizlemede '')" },
|
|
273
|
+
reads: 'setter: self.value da güncellenir, event.data.value da dolu gelir; her tamamlanan çizgide bir kez yayılır'
|
|
274
|
+
},
|
|
275
|
+
clear: { payload: {}, reads: "temizlemede önce change('') yayılır, sonra clear — değeri change'ten oku" }
|
|
276
|
+
},
|
|
277
|
+
stepper: {
|
|
278
|
+
mount: NO_DATA,
|
|
279
|
+
stepChange: {
|
|
280
|
+
payload: { currentStep: 'number (hedef adım, 0 tabanlı)', previous: 'number (önceki adım)' },
|
|
281
|
+
reads: "currentStep self'e yazılır; event.data da dolu gelir"
|
|
282
|
+
},
|
|
283
|
+
finish: { payload: { currentStep: 'number (son adımın indeksi)' }, reads: 'adımı değiştirmez; yalnız son adımda kapı açıkken yayılır' },
|
|
284
|
+
validationFail: {
|
|
285
|
+
payload: { currentStep: 'number (engellenen adım)', previous: 'number (aynı değer)' },
|
|
286
|
+
reads: 'YALNIZ İleri düğmesinden yayılır; engellenen başlık tıklaması sessiz kalır'
|
|
287
|
+
}
|
|
288
|
+
}
|
|
148
289
|
};
|
|
149
290
|
export const eventPayloadNote = "Olay yükü motorda komponentin KENDİ prop'una yazılır (FlowMachine.onEmit → setNode) ve akışta " +
|
|
150
291
|
"self.<anahtar> ile okunur — payload anahtarları aynı zamanda o prop'ların adıdır. " +
|
|
151
|
-
"event.data komponent olaylarında BOŞ gelir: motor akışı emit'in params'ıyla başlatır ve " +
|
|
152
|
-
"event.data = params.data'dır (
|
|
292
|
+
"event.data ESKİ kuşak komponent olaylarında BOŞ gelir: motor akışı emit'in params'ıyla başlatır ve " +
|
|
293
|
+
"event.data = params.data'dır (html actions/fireEvent ile 2026-08-27 kuşağı — eventcalendar · " +
|
|
294
|
+
"importwizard · kanban · signature · stepper — bu yoldan doldurur; ayrım tekil sözleşme notlarında). " +
|
|
153
295
|
"payload boşsa olay veri taşımaz; 'reads' verilmişse değer yükte değil komponentin başka bir prop'undadır. " +
|
|
154
296
|
"self yerine value { type:'property', ref:{ source:'component', element:'<elementId>', property:'<prop>' } } de kullanılabilir. " +
|
|
155
297
|
"payload alanı yoksa yük sözlüğe henüz işlenmemiştir; tahmin etmeyin.";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
const ERROR_STEP_NAME = 'gatewayOnError';
|
|
3
|
+
const INJECTED_KINDS = new Set(['workflow', 'middleware']);
|
|
4
|
+
function asSteps(value) {
|
|
5
|
+
return (Array.isArray(value) ? value : []).filter((step) => Boolean(step) && typeof step === 'object');
|
|
6
|
+
}
|
|
7
|
+
function availableName(steps) {
|
|
8
|
+
const used = new Set(steps.map(step => String(step.name ?? '')));
|
|
9
|
+
if (!used.has(ERROR_STEP_NAME))
|
|
10
|
+
return ERROR_STEP_NAME;
|
|
11
|
+
let suffix = 2;
|
|
12
|
+
while (used.has(`${ERROR_STEP_NAME}${suffix}`))
|
|
13
|
+
suffix += 1;
|
|
14
|
+
return `${ERROR_STEP_NAME}${suffix}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Global-error dalı yalnız START adımında aranır ve tek olmalıdır
|
|
18
|
+
* (WorkflowRuntimeCacheDto.cs:37-38, WorkflowService.cs:454-459 → HTTP 400). Bağlanmamış ya da
|
|
19
|
+
* hiç olmayan fail dalları bu dala kendiliğinden yönlendiği için (IStep.cs:74-86) fail dallarını
|
|
20
|
+
* ayrıca bağlamak gerekmez. Enjekte edilen response adımı global-error modunda ReturnDto'yu
|
|
21
|
+
* atladığı için (ResponseStep.cs:62-64) mevcut sözleşmeyi bozmaz. `automation` dışarıda: o tipte
|
|
22
|
+
* `response` adımı yasak ve tek terminal olan `revert` tetikleyen işlemi geri alır.
|
|
23
|
+
*/
|
|
24
|
+
export function ensureWorkflowErrorPath(definition, kind) {
|
|
25
|
+
if (!kind || !INJECTED_KINDS.has(kind) || !Array.isArray(definition.steps))
|
|
26
|
+
return [];
|
|
27
|
+
const steps = asSteps(definition.steps);
|
|
28
|
+
const startStep = steps.find(step => step.type === 'start');
|
|
29
|
+
if (!startStep)
|
|
30
|
+
return [];
|
|
31
|
+
// Start DIŞI bir adımda global-error dalı varsa dokunma: enjeksiyon start'a ikinci bir dal
|
|
32
|
+
// eklerdi ve WF-022 önce "2 adet 'global-error' dalı var … Tek dal bırakın" derdi — çağıranın
|
|
33
|
+
// hiç yazmadığı dalı suçlayan, yanlış onarıma çağıran bir bulgu. Dokunmayınca WF-022 yalnız
|
|
34
|
+
// gerçek hatayı ("dal start dışı adımda") bildirir.
|
|
35
|
+
if (steps.some(step => step !== startStep && (Array.isArray(step.branches) ? step.branches : []).some(branch => branch?.type === 'global-error')))
|
|
36
|
+
return [];
|
|
37
|
+
const branches = Array.isArray(startStep.branches) ? startStep.branches : [];
|
|
38
|
+
if (branches.some(branch => branch?.type === 'global-error' && branch.nextStepId))
|
|
39
|
+
return [];
|
|
40
|
+
const name = availableName(steps);
|
|
41
|
+
const errorStepId = randomUUID();
|
|
42
|
+
const unwired = branches.find(branch => branch?.type === 'global-error');
|
|
43
|
+
definition.steps.push({
|
|
44
|
+
id: errorStepId,
|
|
45
|
+
name,
|
|
46
|
+
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.",
|
|
48
|
+
caution: "Bu adımı gateway ekledi; silersen akışın hata yolu kaybolur ve patlayan adım istemciye ham hata döndürür.",
|
|
49
|
+
rule: {
|
|
50
|
+
stepType: 'response',
|
|
51
|
+
httpStatusCode: 500,
|
|
52
|
+
bodyExpression: '({ code: error.code, message: error.message })'
|
|
53
|
+
},
|
|
54
|
+
branches: []
|
|
55
|
+
});
|
|
56
|
+
if (unwired) {
|
|
57
|
+
unwired.nextStepId = errorStepId;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
branches.push({ id: randomUUID(), name: 'global-error', type: 'global-error', nextStepId: errorStepId });
|
|
61
|
+
}
|
|
62
|
+
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 ham exception mesajını 500 gövdesine basar; bu adım çıktıyı denetim altına alır. Kendi hata gövdenizi istiyorsanız bu adımın bodyExpression'ını değiştirin.`];
|
|
64
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logStep } from '../observability/trace.js';
|
|
2
|
+
import { ensureWorkflowErrorPath } from './errorPath.js';
|
|
2
3
|
const SORT_DEFAULT = { name: 'sortField', type: 'sortType', source: 'query-string' };
|
|
3
4
|
const LIMIT_DEFAULT = { name: 'limit', offset: 'offset', source: 'query-string' };
|
|
4
5
|
function asArray(value) {
|
|
@@ -164,6 +165,7 @@ export function normalizeWorkflowDefinition(definition) {
|
|
|
164
165
|
const fixes = [];
|
|
165
166
|
const bodies = [definition, definition.automation];
|
|
166
167
|
fillDtoBehaviors(definition, fixes);
|
|
168
|
+
fixes.push(...ensureWorkflowErrorPath(definition, definition.type));
|
|
167
169
|
for (const body of bodies) {
|
|
168
170
|
if (!body || typeof body !== 'object')
|
|
169
171
|
continue;
|
|
@@ -588,17 +590,26 @@ export function clampBlockSizes(nodes, maxSizes) {
|
|
|
588
590
|
* ajanın tahmin ettiği `h` bir üst SINIRA dönüşüyor, kartların içi kırpılmış görünüyordu
|
|
589
591
|
* (ürün sahibi ekran görüntüsü, 2026-08-19: randevu sayfasında üç kartın da içi kesik).
|
|
590
592
|
*
|
|
593
|
+
* Üçüncü değer `fill` ("kalanı doldur", 2026-08-28): yükseklik ne içerikten ne satır
|
|
594
|
+
* sayısından gelir, ebeveynin bıraktığı yerden gelir — ızgarada `height: 100%`, flex
|
|
595
|
+
* sütununda `flex: 1 1 auto`, flex satırında `align-self: stretch`. Tam ekran yerleşim
|
|
596
|
+
* kabuğunun (kök kap ekranı doldurur, yan menü tam boy, slot artan alan) tek kurucu
|
|
597
|
+
* parçası budur, bu yüzden burada **korunmalıdır**: auto'ya çevrilirse kabuk sessizce
|
|
598
|
+
* içeriğine çöker. `zeyrek-frontend`: `packages/ng-apps-screen-builder/src/flex-layout.ts`
|
|
599
|
+
* (`getFlexChildStyle`), `packages/ng-apps-ui/src/shared/render/render.functions.ts`.
|
|
600
|
+
*
|
|
591
601
|
* Ajanın açıkça verdiği `appearance.heightType` korunur; katalogda bulunmayan tip auto sayılır —
|
|
592
602
|
* platformun kendi varsayılanı odur ve `fixed` tahmininin bedeli kırpma, `auto` tahmininin
|
|
593
603
|
* bedeli yalnız fazladan yükseklik.
|
|
594
604
|
*/
|
|
605
|
+
const HEIGHT_TYPES = new Set(['auto', 'fixed', 'fill']);
|
|
595
606
|
export function normalizeHeightTypes(nodes, autoHeights) {
|
|
596
607
|
const stamped = [];
|
|
597
608
|
for (const node of asObjects(nodes)) {
|
|
598
609
|
const appearance = (node.appearance && typeof node.appearance === 'object' && !Array.isArray(node.appearance)
|
|
599
610
|
? node.appearance
|
|
600
611
|
: {});
|
|
601
|
-
if (
|
|
612
|
+
if (!HEIGHT_TYPES.has(String(appearance.heightType ?? ''))) {
|
|
602
613
|
const heightType = autoHeights.get(String(node.type ?? '')) === false ? 'fixed' : 'auto';
|
|
603
614
|
appearance.heightType = heightType;
|
|
604
615
|
node.appearance = appearance;
|
|
@@ -6,6 +6,8 @@ export function workflowOutline(definition) {
|
|
|
6
6
|
id: definition.id,
|
|
7
7
|
slug: definition.slug,
|
|
8
8
|
name: definition.name,
|
|
9
|
+
description: definition.description ?? null,
|
|
10
|
+
caution: definition.caution ?? null,
|
|
9
11
|
type: definition.type,
|
|
10
12
|
parameterDtoId: definition.parameterDtoId ?? null,
|
|
11
13
|
returnDtoId: definition.returnDtoId ?? null,
|
|
@@ -13,6 +15,8 @@ export function workflowOutline(definition) {
|
|
|
13
15
|
steps: asArray(definition.steps).map(step => ({
|
|
14
16
|
id: step.id,
|
|
15
17
|
name: step.name,
|
|
18
|
+
description: step.description ?? null,
|
|
19
|
+
caution: step.caution ?? null,
|
|
16
20
|
type: step.type,
|
|
17
21
|
branches: asArray(step.branches).map(branch => ({
|
|
18
22
|
name: branch.name,
|
|
@@ -30,6 +34,8 @@ export function microserviceOutline(definition) {
|
|
|
30
34
|
id: definition.id,
|
|
31
35
|
slug: definition.slug,
|
|
32
36
|
name: definition.name,
|
|
37
|
+
description: definition.description ?? null,
|
|
38
|
+
caution: definition.caution ?? null,
|
|
33
39
|
type: definition.type,
|
|
34
40
|
httpMethod: definition.httpMethod,
|
|
35
41
|
source: definition.source,
|
|
@@ -64,10 +70,11 @@ export function entityOutline(definition) {
|
|
|
64
70
|
dbType: field.dbType,
|
|
65
71
|
nullable: field.nullable,
|
|
66
72
|
relationType: field.relationType ?? 'none',
|
|
67
|
-
relationId: field.relationId ?? null
|
|
73
|
+
relationId: field.relationId ?? null,
|
|
74
|
+
isLocalized: Boolean(field.isLocalized)
|
|
68
75
|
})),
|
|
69
76
|
view: 'outline',
|
|
70
|
-
hint: "Bir alanın tam gövdesi için view:'part', part:'fields[slug=<slug>]'; değişiklik için entity_patch."
|
|
77
|
+
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'."
|
|
71
78
|
};
|
|
72
79
|
}
|
|
73
80
|
const segmentPattern = /^(\w+)(?:\[(?:(\d+)|(\w+)=([^\]]+))\])?$/;
|
package/dist/knowledge/rules.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { reservedTenantTables } from './reservedTables.js';
|
|
2
2
|
const terminalStepTypes = new Set(['response', 'convert', 'revert', 'end']);
|
|
3
|
+
const DESCRIPTION_EXEMPT_STEPS = new Set(['start', 'end']);
|
|
3
4
|
const stepKindMatrix = {
|
|
4
5
|
start: ['workflow', 'automation', 'middleware'],
|
|
5
6
|
condition: ['workflow', 'automation', 'middleware'],
|
|
@@ -34,9 +35,41 @@ function workflowBody(definition, kind) {
|
|
|
34
35
|
function workflowSteps(definition) {
|
|
35
36
|
return asArray(definition.steps).filter((step) => Boolean(step) && typeof step === 'object');
|
|
36
37
|
}
|
|
38
|
+
function hasText(value) {
|
|
39
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
40
|
+
}
|
|
37
41
|
function stepLabel(step) {
|
|
38
42
|
return step.name || step.type || step.id || '?';
|
|
39
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Start'tan başlayıp `global-error` dalları HARİÇ tüm dalları izleyerek erişilen adımlar.
|
|
46
|
+
* Gateway her `workflow`/`middleware` tanımına global-error dalının ucunda bir `response` adımı
|
|
47
|
+
* enjekte ettiği için (`errorPath.ts`, normalize evaluatePolicy'den ÖNCE koşar) "hiç response
|
|
48
|
+
* adımı var mı?" sorusu artık her zaman evet; anlamlı olan soru "BAŞARILI çağrıya bir şey
|
|
49
|
+
* dönüyor mu?" — o da yalnız bu küme üzerinden yanıtlanabilir.
|
|
50
|
+
*/
|
|
51
|
+
function successPathSteps(steps) {
|
|
52
|
+
const start = steps.find(step => step.type === 'start');
|
|
53
|
+
if (!start)
|
|
54
|
+
return [];
|
|
55
|
+
const byId = new Map(steps.filter(step => step.id).map(step => [String(step.id), step]));
|
|
56
|
+
const reached = new Set([start]);
|
|
57
|
+
const queue = [start];
|
|
58
|
+
while (queue.length > 0) {
|
|
59
|
+
const step = queue.pop();
|
|
60
|
+
for (const branch of asArray(step.branches)) {
|
|
61
|
+
const candidate = branch;
|
|
62
|
+
if (!candidate || candidate.type === 'global-error' || !candidate.nextStepId)
|
|
63
|
+
continue;
|
|
64
|
+
const next = byId.get(String(candidate.nextStepId));
|
|
65
|
+
if (!next || reached.has(next))
|
|
66
|
+
continue;
|
|
67
|
+
reached.add(next);
|
|
68
|
+
queue.push(next);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return [...reached];
|
|
72
|
+
}
|
|
40
73
|
/**
|
|
41
74
|
* "Guard'lı mı?" sorusunun tek doğru cevabı YAZIM BİÇİMİ DEĞİL, SIRA: kökün kendisine (üye
|
|
42
75
|
* erişimi olmadan) bir kez dokunulduğu an — `Page.user &&`, `Page.user || {}`, `!Page.user`,
|
|
@@ -206,6 +239,33 @@ function tableSelectionFindings(ruleId, owner, parameters, guide) {
|
|
|
206
239
|
}
|
|
207
240
|
return [];
|
|
208
241
|
}
|
|
242
|
+
function escapeForPattern(value) {
|
|
243
|
+
return value.replace(/[^A-Za-z0-9_]/g, '\\$&');
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* `selectFields` disiplininin ikinci yarısı: BOŞ seçim tüm alanları getirir (WF-007), FAZLA seçim
|
|
247
|
+
* ise okunmayan alanları her satırda taşır. Bu ikincisi yalnız workflow'da ölçülebilir — table
|
|
248
|
+
* adımının çıktısını akışın KENDİSİ tüketir (microservice'te seçim ucun sözleşmesidir, dışarıda
|
|
249
|
+
* kimin ne okuduğu bilinmez). Ölçüm metinseldir: adımın dışındaki tüm gövde tek metne çevrilir,
|
|
250
|
+
* her adımın kendi `selectFields` BİLDİRİMİ elenir (bildirim kullanım değildir) ve alan slug'ı bu
|
|
251
|
+
* metinde hiç geçmiyorsa okunmuyor sayılır. Adımın çıktısı bütün olarak geçiriliyorsa
|
|
252
|
+
* (`{ list: rows.data }` — `.data`'dan sonra alan erişimi yok) her alan meşrudur, adım ölçülmez.
|
|
253
|
+
*/
|
|
254
|
+
function unreadSelectFields(step, steps) {
|
|
255
|
+
const name = String(step.name ?? '').trim();
|
|
256
|
+
if (name === '')
|
|
257
|
+
return [];
|
|
258
|
+
const escapedName = escapeForPattern(name);
|
|
259
|
+
const usage = JSON.stringify(steps.filter(other => other !== step), (key, value) => (key === 'selectFields' ? undefined : value)) ?? '';
|
|
260
|
+
if (!new RegExp(`\\b${escapedName}\\b`).test(usage))
|
|
261
|
+
return [];
|
|
262
|
+
if (new RegExp(`\\b${escapedName}\\s*\\.\\s*data(?!\\s*[.[])`).test(usage))
|
|
263
|
+
return [];
|
|
264
|
+
return asArray(step.rule?.selectFields)
|
|
265
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
266
|
+
.map(field => String(field.fieldSlug ?? field.slug ?? ''))
|
|
267
|
+
.filter(slug => slug.length > 0 && !new RegExp(`\\b${escapeForPattern(slug)}\\b`).test(usage));
|
|
268
|
+
}
|
|
209
269
|
export const policyRules = [
|
|
210
270
|
{
|
|
211
271
|
id: 'MS-001',
|
|
@@ -226,6 +286,20 @@ export const policyRules = [
|
|
|
226
286
|
return [];
|
|
227
287
|
}
|
|
228
288
|
},
|
|
289
|
+
{
|
|
290
|
+
id: 'MS-014',
|
|
291
|
+
module: 'microservice',
|
|
292
|
+
appliesTo: ['create', 'update'],
|
|
293
|
+
sourceRef: "WF-024'ün microservice karşılığı: uç listesinde ad ve slug var, 'bu uç ne işe yarar' yok. Alan: Microservice.Description (nullable, çalışma zamanı okumaz)",
|
|
294
|
+
check: definition => hasText(definition.description)
|
|
295
|
+
? []
|
|
296
|
+
: [{
|
|
297
|
+
ruleId: 'MS-014',
|
|
298
|
+
severity: 'block',
|
|
299
|
+
message: "Ucun 'description' alanı boş. 1-2 cümleyle bu ucun ne işe yaradığını yazın — Studio'da servis formunda görünür ve ucu devralan geliştiricinin ilk okuduğu şeydir. Çağıranın ne elde ettiğini yazın ('anket başlığı + sorularını tek çağrıda kaydeder'), kaynağın şeklini değil ('workflow kaynaklı POST ucu').",
|
|
300
|
+
guide: 'ngapps://guides/microservice'
|
|
301
|
+
}]
|
|
302
|
+
},
|
|
229
303
|
{
|
|
230
304
|
id: 'MS-002',
|
|
231
305
|
module: 'microservice',
|
|
@@ -321,15 +395,18 @@ export const policyRules = [
|
|
|
321
395
|
id: 'WF-004',
|
|
322
396
|
module: 'workflow',
|
|
323
397
|
appliesTo: ['create', 'update'],
|
|
324
|
-
sourceRef:
|
|
398
|
+
sourceRef: "UI WORKFLOW_STEPS: Response modülleri workflow+middleware. Erişilebilirlik global-error dalını dışlar: o dalın ucundaki response YALNIZ hata yolunda çalışır (ResponseStep.cs:62-64 orada ReturnDto'yu atlar) ve gateway'in kendi enjeksiyonu da oraya bağlanır (errorPath.ts)",
|
|
325
399
|
check: (definition, context) => {
|
|
326
400
|
const body = workflowBody(definition, context.workflowKind);
|
|
327
401
|
const kind = context.workflowKind ?? body.type;
|
|
328
|
-
|
|
402
|
+
const steps = workflowSteps(body);
|
|
403
|
+
if (kind !== 'workflow' || !steps.some(step => step.type === 'start'))
|
|
404
|
+
return [];
|
|
405
|
+
if (!successPathSteps(steps).some(step => step.type === 'response')) {
|
|
329
406
|
return [{
|
|
330
407
|
ruleId: 'WF-004',
|
|
331
408
|
severity: 'warn',
|
|
332
|
-
message: "type='workflow' ama response step'i yok — akış yanıt döndürmeyecek.
|
|
409
|
+
message: "type='workflow' ama BAŞARI yolunda response step'i yok — akış başarıyla biten çağrıya yanıt döndürmeyecek. Yalnız 'global-error' dalının ucunda response bulunması yetmez: o adım sadece akış patladığında çalışır. Start'tan erişilebilen bir dalın sonuna response step'i bağlayın (adım tanımlı ama hiçbir dalın 'nextStepId' hedefi değilse de bu uyarı çıkar).",
|
|
333
410
|
guide: 'ngapps://guides/workflow'
|
|
334
411
|
}];
|
|
335
412
|
}
|
|
@@ -402,6 +479,56 @@ export const policyRules = [
|
|
|
402
479
|
.map(finding => ({ ...finding, severity: 'warn' })));
|
|
403
480
|
}
|
|
404
481
|
},
|
|
482
|
+
{
|
|
483
|
+
id: 'WF-023',
|
|
484
|
+
module: 'workflow',
|
|
485
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
486
|
+
sourceRef: "MS-001/WF-007'nin ikinci yarısı ('yalnız ihtiyaç duyulan alanlar seçilir'): boş seçimi WF-007 yakalıyordu, FAZLA seçim denetimsizdi. Yalnız okuma operasyonlarında ve yalnız workflow'da — write step'te platform selectFields'i zaten yok sayar, microservice'te ise seçim ucun sözleşmesidir",
|
|
487
|
+
check: (definition, context) => {
|
|
488
|
+
const steps = workflowSteps(workflowBody(definition, context.workflowKind));
|
|
489
|
+
return steps
|
|
490
|
+
.filter(step => step.type === 'table' && isReadOperation(step.rule))
|
|
491
|
+
.flatMap((step) => {
|
|
492
|
+
const unread = [...new Set(unreadSelectFields(step, steps))];
|
|
493
|
+
return unread.length > 0
|
|
494
|
+
? [{
|
|
495
|
+
ruleId: 'WF-023',
|
|
496
|
+
severity: 'warn',
|
|
497
|
+
message: `'${stepLabel(step)}' table step'i şu alanları seçiyor ama akışın hiçbir yerinde okunmuyor: ${unread.join(', ')} — seçilen her alan sorguya ve taşınan satıra maliyet ekler, join'li adımda bu maliyet katlanır. selectFields'ta yalnız gerçekten kullanılan alanları bırakın. (Satırları alan adı yazmadan dolaşan bir code adımı varsa uyarı yanlış pozitiftir; tespit metinseldir.)`,
|
|
498
|
+
guide: 'ngapps://guides/workflow'
|
|
499
|
+
}]
|
|
500
|
+
: [];
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: 'WF-024',
|
|
506
|
+
module: 'workflow',
|
|
507
|
+
appliesTo: ['create', 'update'],
|
|
508
|
+
sourceRef: "Studio kanvası adım adını gösterir, ne yaptığını göstermez: 11 satırlık bir code adımının işini anlamak için ifadeyi satır satır okumak gerekiyordu. Açıklamayı üreten taraf (bu gateway) biliyor ama hiçbir yere yazmıyordu. Alan: Step.Description / Workflow.Description (nullable, motor okumaz). 'preview' dışarıda: kaydedilmeyen bir koşumu açıklama eksikliği yüzünden durdurmak yazımı yavaşlatır, kalıcı kayıt üretmez",
|
|
509
|
+
check: (definition, context) => {
|
|
510
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
511
|
+
const undescribed = workflowSteps(body)
|
|
512
|
+
.filter(step => !DESCRIPTION_EXEMPT_STEPS.has(String(step.type)))
|
|
513
|
+
.filter(step => !hasText(step.description))
|
|
514
|
+
.map(stepLabel);
|
|
515
|
+
const flowMissing = !hasText(definition.description);
|
|
516
|
+
if (undescribed.length === 0 && !flowMissing)
|
|
517
|
+
return [];
|
|
518
|
+
const parts = [
|
|
519
|
+
undescribed.length > 0
|
|
520
|
+
? `şu adımlarda 'description' yok: ${undescribed.join(', ')}`
|
|
521
|
+
: '',
|
|
522
|
+
flowMissing ? "akışın kendi açıklaması ('description') boş" : ''
|
|
523
|
+
].filter(Boolean);
|
|
524
|
+
return [{
|
|
525
|
+
ruleId: 'WF-024',
|
|
526
|
+
severity: 'block',
|
|
527
|
+
message: `Açıklama eksik — ${parts.join('; ')}. Her adıma ve akışın kendisine 1-2 cümle 'description' yazın: Studio kanvasında adımın yanında ⓘ ikonu olarak görünür ve akışı devralan geliştiricinin tek okuma kaynağıdır. NE YAPTIĞINI değil NEDEN olduğunu yazın: 'assignments tablosunu status=Aktif ile okur' mekanizmadır ve zaten koddan okunuyor; 'kullanıcının asal pozisyonunu seçer, birden fazla aktif atama varsa is_primary olanı tercih eder' niyettir. start/end adımları muaftır.`,
|
|
528
|
+
guide: 'ngapps://guides/workflow'
|
|
529
|
+
}];
|
|
530
|
+
}
|
|
531
|
+
},
|
|
405
532
|
{
|
|
406
533
|
id: 'WF-008',
|
|
407
534
|
module: 'workflow',
|
|
@@ -1700,6 +1827,60 @@ export const policyRules = [
|
|
|
1700
1827
|
});
|
|
1701
1828
|
}
|
|
1702
1829
|
},
|
|
1830
|
+
{
|
|
1831
|
+
id: 'WF-022',
|
|
1832
|
+
module: 'workflow',
|
|
1833
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1834
|
+
sourceRef: "WorkflowService.cs:454-459 — birden fazla global-error dalı MultipleGlobalErrorBranchException, start dışı dal GlobalErrorBranchMustBeOnStartStepException (ikisi de HTTP 400: ExceptionHandler.cs:145-146). Runtime dalı yalnız start adımında arar: WorkflowRuntimeCacheDto.cs:37-38",
|
|
1835
|
+
check: (definition, context) => {
|
|
1836
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
1837
|
+
const steps = workflowSteps(body);
|
|
1838
|
+
const findings = [];
|
|
1839
|
+
const carriers = steps.filter(step => asArray(step.branches).some(branch => branch?.type === 'global-error'));
|
|
1840
|
+
const total = carriers.reduce((count, step) => count + asArray(step.branches).filter(branch => branch?.type === 'global-error').length, 0);
|
|
1841
|
+
if (total > 1) {
|
|
1842
|
+
findings.push({
|
|
1843
|
+
ruleId: 'WF-022',
|
|
1844
|
+
severity: 'block',
|
|
1845
|
+
message: `${total} adet 'global-error' dalı var (${carriers.map(stepLabel).join(', ')}); platform tek dal kabul eder, fazlasını 400 ile reddeder. Tek dal bırakın — hata yolu zaten TEK bir noktadan başlar, dallanmayı hedef adımın içinde yapın.`,
|
|
1846
|
+
guide: 'ngapps://guides/workflow'
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
const misplaced = carriers.filter(step => step.type !== 'start');
|
|
1850
|
+
if (misplaced.length > 0) {
|
|
1851
|
+
findings.push({
|
|
1852
|
+
ruleId: 'WF-022',
|
|
1853
|
+
severity: 'block',
|
|
1854
|
+
message: `'global-error' dalı start dışı adımda: ${misplaced.map(stepLabel).join(', ')}. Runtime bu dalı YALNIZ start adımında arar; başka adımda duran dal 400 ile reddedilir (kayıt olsa bile hiç çalışmazdı). Dalı start adımının branches[] dizisine taşıyın; adım bazlı hata yakalamak için o adımın 'fail' dalını bağlayın.`,
|
|
1855
|
+
guide: 'ngapps://guides/workflow'
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
return findings;
|
|
1859
|
+
}
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
id: 'WF-021',
|
|
1863
|
+
module: 'workflow',
|
|
1864
|
+
appliesTo: ['create', 'update'],
|
|
1865
|
+
sourceRef: "IStep.cs:74-86 bağlanmamış/eksik fail dalını global-error dalına yönlendirir; ikisi de yoksa ActivityContext.cs:176-183 ham exception mesajını 500 gövdesine basar. automation'da 'response' adımı yasak (stepKindMatrix) olduğu için gateway bu tipte enjeksiyon yapmaz — errorPath.ts",
|
|
1866
|
+
check: (definition, context) => {
|
|
1867
|
+
if (context.workflowKind !== 'automation')
|
|
1868
|
+
return [];
|
|
1869
|
+
const steps = workflowSteps(workflowBody(definition, context.workflowKind));
|
|
1870
|
+
const wired = steps.some(step => asArray(step.branches).some(branch => {
|
|
1871
|
+
const candidate = branch;
|
|
1872
|
+
return (candidate?.type === 'global-error' || candidate?.type === 'fail') && Boolean(candidate.nextStepId);
|
|
1873
|
+
}));
|
|
1874
|
+
if (wired)
|
|
1875
|
+
return [];
|
|
1876
|
+
return [{
|
|
1877
|
+
ruleId: 'WF-021',
|
|
1878
|
+
severity: 'warn',
|
|
1879
|
+
message: "Automation'ın hiç bağlı hata yolu yok. Gateway workflow/middleware'e otomatik hata adımı ekler ama automation'da 'response' adımı YASAKTIR, bu yüzden hata yolunu siz yazmalısınız: start adımına { type: 'global-error', nextStepId: <adım> } dalı koyup hedefe 'revert' (tetikleyen satır işlemini geri alır) ya da 'email'/'table' adımı bağlayın. Hata yolu yokken adım patladığında automation sessizce durur ve geriye iz kalmaz.",
|
|
1880
|
+
guide: 'ngapps://guides/workflow'
|
|
1881
|
+
}];
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1703
1884
|
{
|
|
1704
1885
|
id: 'MW-003',
|
|
1705
1886
|
module: 'workflow',
|