@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.
Files changed (39) hide show
  1. package/.env.example +21 -7
  2. package/.env.production.example +42 -0
  3. package/README.md +14 -7
  4. package/dist/config.js +113 -46
  5. package/dist/knowledge/componentCatalog.js +476 -32
  6. package/dist/knowledge/componentEvents.js +154 -12
  7. package/dist/knowledge/errorPath.js +64 -0
  8. package/dist/knowledge/normalize.js +12 -1
  9. package/dist/knowledge/outlines.js +9 -2
  10. package/dist/knowledge/rules.js +184 -3
  11. package/dist/knowledge/styleResolve.js +485 -0
  12. package/dist/observability/trace.js +4 -0
  13. package/dist/platform/http.js +3 -0
  14. package/dist/platform/responses.js +80 -4
  15. package/dist/platform/screenshot.js +106 -0
  16. package/dist/resources/examples/layout-app-shell.json +368 -0
  17. package/dist/resources/examples/microservice-table-list.json +1 -0
  18. package/dist/resources/examples/middleware-authenticate.json +243 -47
  19. package/dist/resources/examples/workflow-basic.json +24 -2
  20. package/dist/resources/guides/component.md +6 -3
  21. package/dist/resources/guides/entity.md +33 -1
  22. package/dist/resources/guides/external-component.md +129 -0
  23. package/dist/resources/guides/layout.md +147 -0
  24. package/dist/resources/guides/microservice.md +35 -0
  25. package/dist/resources/guides/middleware.md +10 -0
  26. package/dist/resources/guides/page.md +137 -15
  27. package/dist/resources/guides/translation.md +80 -0
  28. package/dist/resources/guides/workflow.md +127 -2
  29. package/dist/server.js +4 -0
  30. package/dist/tools/externalComponents.js +285 -0
  31. package/dist/tools/layouts.js +48 -6
  32. package/dist/tools/microservices.js +2 -2
  33. package/dist/tools/pages.js +98 -30
  34. package/dist/tools/platformInfo.js +2 -2
  35. package/dist/tools/projects.js +7 -2
  36. package/dist/tools/screens.js +36 -0
  37. package/dist/tools/translations.js +37 -0
  38. package/dist/tools/workflows.js +3 -3
  39. 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
- numberinput: { mount: NO_DATA, change: valueChange('number'), blur: NO_DATA },
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 (badge · breadcrumb · jsonviewer ·
74
- // notification · progress · qrcode · slot · tags · text) bilinçli olarak kayıtsızdır.
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
- rating: { mount: NO_DATA, change: valueChange('number (0..max; allowHalf ile yarım)') },
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 (yalnız html actions/fireEvent bu yoldan doldurur). " +
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 (appearance.heightType !== 'auto' && appearance.heightType !== 'fixed') {
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+)=([^\]]+))\])?$/;
@@ -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: 'UI WORKFLOW_STEPS: Response modülleri workflow+middleware',
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
- if (kind === 'workflow' && !workflowSteps(body).some(step => step.type === 'response')) {
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. Bilinçli değilse akışın sonuna response step'i bağlayın.",
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',