@appaflytech/zeyrek-ai-mcp 0.0.3 → 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 (35) 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 -47
  5. package/dist/knowledge/componentCatalog.js +474 -31
  6. package/dist/knowledge/componentEvents.js +150 -12
  7. package/dist/knowledge/errorPath.js +64 -0
  8. package/dist/knowledge/normalize.js +12 -1
  9. package/dist/knowledge/outlines.js +6 -0
  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/external-component.md +129 -0
  22. package/dist/resources/guides/layout.md +147 -0
  23. package/dist/resources/guides/microservice.md +35 -0
  24. package/dist/resources/guides/middleware.md +10 -0
  25. package/dist/resources/guides/page.md +132 -14
  26. package/dist/resources/guides/workflow.md +127 -2
  27. package/dist/server.js +2 -0
  28. package/dist/tools/externalComponents.js +285 -0
  29. package/dist/tools/layouts.js +48 -6
  30. package/dist/tools/microservices.js +2 -2
  31. package/dist/tools/pages.js +98 -30
  32. package/dist/tools/platformInfo.js +2 -2
  33. package/dist/tools/screens.js +36 -0
  34. package/dist/tools/workflows.js +3 -3
  35. package/package.json +4 -1
@@ -0,0 +1,485 @@
1
+ /**
2
+ * ÖZET — Bir düğümün EFEKTİF stili: tasarımcının ekranda gördüğü sonucun
3
+ * gateway tarafında yeniden kurulması. `page_runtime_get(resolveStyles: true)`
4
+ * her düğüme bu modülün ürettiği `resolvedStyle` + `styleLayers`'ı ekler ve
5
+ * "bu kutu neden böyle görünüyor" sorusu cevaplanabilir olur.
6
+ *
7
+ * KATMAN SIRASI (karar K4, 2026-08-30) — tipli stil komponentlerinde:
8
+ * tema componentStyles → $preset = TABAN (varsayılan verir)
9
+ * tipli gruplar (panel seçimleri) = ortada (temayı alan alan ezer)
10
+ * düğümün serbest `style`'ı = EN ÜST (kaçış kapısı, son söz)
11
+ * Tipli olmayan komponentlerde eski birleşik kaskad geçerlidir
12
+ * (componentStyles → preset → düğüm).
13
+ *
14
+ * KAYNAK EŞLEŞMESİ: kaskad `zeyrek-frontend/packages/ng-apps-screen-builder/
15
+ * src/style-presets.ts`'in, tipli derleme `zeyrek-frontend/packages/ng-apps-ui/
16
+ * src/style-groups/style-group-compose.ts`'in (aibox bölge uzantısı
17
+ * `.../components/aibox/aibox-style-logic.ts`) birebir kopyasıdır. Gateway
18
+ * frontend workspace'ine dahil olmadığı için paket import edilemez; kopyanın
19
+ * sürüklenmesini `test/style-resolve.test.ts` üç yoldan kilitler: kaynak metin
20
+ * ipleri, ortak ALTIN vakalar (`style-group-golden.json`) ve `components.json`
21
+ * ile ALAN pariteleri. Yeni bir komponent tipli gruplara katıldığında buradaki
22
+ * `TYPED_STYLE_COMPONENTS` kaydına eklenir — başka hiçbir yer değişmez.
23
+ */
24
+ export const STYLE_PRESET_KEY = '$preset';
25
+ /**
26
+ * Satır yüksekliği ÜÇ yazımı da kabul eder: oran (1.4), yüzde ('140%') ve CSS
27
+ * uzunluğu ('24px'). Çıplak sayı ikircikli tek durumdur — eski panel YÜZDE
28
+ * yazıyordu (140 → 1.4), bu yüzden 4'ten büyük çıplak sayı yüzde sayılır.
29
+ */
30
+ export const asLineHeight = (value) => {
31
+ if (!isSet(value))
32
+ return undefined;
33
+ const text = String(value).trim();
34
+ if (!text)
35
+ return undefined;
36
+ const numeric = Number(text);
37
+ if (Number.isFinite(numeric)) {
38
+ if (numeric <= 0)
39
+ return undefined;
40
+ return numeric >= 4 ? numeric / 100 : numeric;
41
+ }
42
+ return text;
43
+ };
44
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
45
+ export const deepMergeStyle = (base, override) => {
46
+ const result = { ...base };
47
+ for (const key in override) {
48
+ const next = override[key];
49
+ const current = result[key];
50
+ result[key] = isPlainObject(current) && isPlainObject(next) ? deepMergeStyle(current, next) : next;
51
+ }
52
+ return result;
53
+ };
54
+ export const resolveNodeStyleLayers = (style, source, nodeType) => {
55
+ const styleObject = isPlainObject(style) ? style : undefined;
56
+ const { [STYLE_PRESET_KEY]: presetName, ...own } = styleObject ?? {};
57
+ const componentStyle = nodeType && isPlainObject(source?.componentStyles?.[nodeType]) ? source.componentStyles[nodeType] : undefined;
58
+ const preset = typeof presetName === 'string' && isPlainObject(source?.presets?.[presetName]) ? source.presets[presetName] : undefined;
59
+ let theme;
60
+ if (componentStyle || preset) {
61
+ theme = deepMergeStyle(componentStyle ?? {}, preset ?? {});
62
+ delete theme[STYLE_PRESET_KEY];
63
+ }
64
+ if (!styleObject && typeof style === 'string' && style)
65
+ return { theme, own: style };
66
+ return { theme, own: Object.keys(own).length ? own : undefined };
67
+ };
68
+ export const resolveNodeStyle = (style, source, nodeType) => {
69
+ if (!isPlainObject(style) && style !== undefined && style !== null)
70
+ return style;
71
+ const { theme, own } = resolveNodeStyleLayers(style, source, nodeType);
72
+ const result = deepMergeStyle(theme ?? {}, isPlainObject(own) ? own : {});
73
+ return Object.keys(result).length ? result : undefined;
74
+ };
75
+ /* ---- Tipli grup derlemesi (style-group-compose.ts kopyası) ---- */
76
+ // Kademeler tema ölçeğinin token'larıdır (frontend T.1 ile birebir):
77
+ // tema "Border radius"u değişince sm/lg oransal izler, kaynak tek yerdedir.
78
+ const RADIUS_VALUES = {
79
+ none: '0',
80
+ sm: 'var(--ng-radius-sm)',
81
+ md: 'var(--ng-radius-md)',
82
+ lg: 'var(--ng-radius-lg)',
83
+ pill: 'var(--ng-radius-pill)'
84
+ };
85
+ const SHADOW_VALUES = {
86
+ none: 'none',
87
+ sm: 'var(--ng-shadow-sm)',
88
+ md: 'var(--ng-shadow-md)',
89
+ lg: 'var(--ng-shadow-lg)'
90
+ };
91
+ const DEFAULT_DIVIDER_COLOR = 'var(--ng-border-primary)';
92
+ const isUnresolvedExpression = (value) => typeof value === 'string' && value.includes('{{');
93
+ const isSet = (value) => value !== undefined && value !== null && value !== '' && !isUnresolvedExpression(value);
94
+ const asNumber = (value) => {
95
+ if (!isSet(value))
96
+ return undefined;
97
+ const parsed = Number(value);
98
+ return Number.isFinite(parsed) ? parsed : undefined;
99
+ };
100
+ /**
101
+ * DEĞER MODELİ (K9): boyut alanları CSS METNİ taşır ('18px', '1.2em', '50%');
102
+ * çıplak sayı px sayılır (eski sayfa toleransı); sıfır ve boşluk "seçilmedi".
103
+ */
104
+ const asDimension = (value) => {
105
+ if (!isSet(value))
106
+ return undefined;
107
+ if (typeof value === 'number')
108
+ return Number.isFinite(value) && value !== 0 ? `${value}px` : undefined;
109
+ const text = String(value).trim();
110
+ if (!text)
111
+ return undefined;
112
+ const numeric = Number(text);
113
+ // METİN sıfır gerçek seçimdir (bileşenin taban dolgusunu kaldırmanın tek
114
+ // yolu); yalnız SAYI 0 "seçilmedi" sayılır (eski sayı girdisi toleransı).
115
+ if (Number.isFinite(numeric))
116
+ return `${numeric}px`;
117
+ return text;
118
+ };
119
+ const assign = (target, key, value) => {
120
+ if (isSet(value))
121
+ target[key] = value;
122
+ };
123
+ const nest = (target, selector, rules) => {
124
+ if (!Object.keys(rules).length)
125
+ return;
126
+ target[selector] = isPlainObject(target[selector]) ? deepMergeStyle(target[selector], rules) : rules;
127
+ };
128
+ /** `radius` TEK alandır: preset anahtarı ('lg') YA DA CSS uzunluğu ('18px'). */
129
+ const resolveRadius = (value) => {
130
+ if (!isSet(value))
131
+ return undefined;
132
+ return RADIUS_VALUES[value] ?? asDimension(value);
133
+ };
134
+ /**
135
+ * Gölge TEK alanda iki yazımı taşır: eski preset anahtarı ('md') tema
136
+ * token'ına çevrilir, geri kalan her şey (çok katmanlı `box-shadow`, `inset`,
137
+ * `var(--ng-shadow-lg)`) olduğu gibi CSS'e iner.
138
+ */
139
+ const resolveShadow = (shadow) => {
140
+ if (!isSet(shadow))
141
+ return undefined;
142
+ return SHADOW_VALUES[shadow] ?? String(shadow);
143
+ };
144
+ /**
145
+ * Grupların alan listeleri — frontend `style-groups/style-group-schemas.ts`
146
+ * `STYLE_GROUP_FIELDS`'in kopyası. Parite testi `components.json`'daki gerçek
147
+ * şemayı bu listeyle karşılaştırır: frontend'e eklenen bir alan buraya
148
+ * yansıtılmazsa test kırmızıya düşer.
149
+ */
150
+ export const STYLE_GROUP_FIELDS = {
151
+ surfaceStyle: [
152
+ 'background',
153
+ 'radius',
154
+ 'borderTop',
155
+ 'borderRight',
156
+ 'borderBottom',
157
+ 'borderLeft',
158
+ 'shadow',
159
+ 'contentOverflow'
160
+ ],
161
+ typographyStyle: [
162
+ 'textColor',
163
+ 'fontFamily',
164
+ 'fontSize',
165
+ 'fontWeight',
166
+ 'fontStyle',
167
+ 'textDecoration',
168
+ 'lineHeight',
169
+ 'letterSpacing',
170
+ 'textAlign'
171
+ ],
172
+ hoverStyle: ['background', 'borderColor', 'shadow', 'lift', 'transitionMs']
173
+ };
174
+ export function composeTypedStyle(input, ext) {
175
+ const groups = {};
176
+ if (isPlainObject(input.surfaceStyle)) {
177
+ const surface = input.surfaceStyle;
178
+ assign(groups, 'background', surface.background);
179
+ assign(groups, 'borderRadius', resolveRadius(surface.radius));
180
+ assign(groups, 'boxShadow', resolveShadow(surface.shadow));
181
+ // Kenarlık YÖN BAŞINA yazılır (CSS'in `border` kısayolu yön başına değer
182
+ // taşıyamaz). Hiçbir yön yazılmamışsa EMEKLİ üçlü okunur — kayıtlı
183
+ // sayfalar bozulmasın diye; bir yön yazıldığı anda takma ad susar.
184
+ let wroteSide = false;
185
+ for (const side of ['borderTop', 'borderRight', 'borderBottom', 'borderLeft']) {
186
+ if (isSet(surface[side])) {
187
+ groups[side] = surface[side];
188
+ wroteSide = true;
189
+ }
190
+ }
191
+ if (!wroteSide) {
192
+ const width = asNumber(surface.borderWidth);
193
+ if (width !== undefined) {
194
+ // 0 geçerli bir seçimdir: "çerçeveyi kapat" demektir, atlanamaz.
195
+ groups.border = width > 0 ? `${width}px ${surface.borderStyle || 'solid'} ${surface.borderColor || DEFAULT_DIVIDER_COLOR}` : 'none';
196
+ }
197
+ else if (isSet(surface.borderColor) || isSet(surface.borderStyle)) {
198
+ groups.border = `1px ${surface.borderStyle || 'solid'} ${surface.borderColor || DEFAULT_DIVIDER_COLOR}`;
199
+ }
200
+ }
201
+ if (isSet(surface.contentOverflow) && ext?.overflowTarget) {
202
+ nest(groups, ext.overflowTarget, { overflow: surface.contentOverflow });
203
+ }
204
+ }
205
+ if (isPlainObject(input.typographyStyle)) {
206
+ const typography = input.typographyStyle;
207
+ assign(groups, 'color', typography.textColor);
208
+ assign(groups, 'fontFamily', typography.fontFamily);
209
+ assign(groups, 'textAlign', typography.textAlign);
210
+ assign(groups, 'fontStyle', typography.fontStyle);
211
+ assign(groups, 'textDecoration', typography.textDecoration);
212
+ assign(groups, 'fontSize', asDimension(typography.fontSize));
213
+ assign(groups, 'letterSpacing', asDimension(typography.letterSpacing));
214
+ if (isSet(typography.fontWeight))
215
+ groups.fontWeight = String(typography.fontWeight);
216
+ const lineHeight = asLineHeight(typography.lineHeight);
217
+ if (lineHeight !== undefined)
218
+ groups.lineHeight = lineHeight;
219
+ }
220
+ if (isPlainObject(input.hoverStyle)) {
221
+ const hover = input.hoverStyle;
222
+ const rules = {};
223
+ assign(rules, 'background', hover.background);
224
+ assign(rules, 'borderColor', hover.borderColor);
225
+ assign(rules, 'boxShadow', resolveShadow(hover.shadow));
226
+ const lift = asDimension(hover.lift);
227
+ if (lift !== undefined)
228
+ rules.transform = `translateY(calc(-1 * ${lift}))`;
229
+ if (Object.keys(rules).length) {
230
+ const duration = asNumber(hover.transitionMs) ?? 160;
231
+ groups.transition = `background-color ${duration}ms ease, border-color ${duration}ms ease, box-shadow ${duration}ms ease, transform ${duration}ms ease`;
232
+ nest(groups, '&:hover', rules);
233
+ }
234
+ }
235
+ ext?.extra?.(input, groups);
236
+ // K4: tema tabandır, gruplar üstüne alan alan biner.
237
+ const themed = isPlainObject(input.themeStyle) ? deepMergeStyle(input.themeStyle, groups) : groups;
238
+ // Metin `style` çizilemez (designerStyle nesne olmayanı eler); atılır,
239
+ // tema + gruplar korunur.
240
+ if (typeof input.style === 'string')
241
+ return Object.keys(themed).length ? themed : undefined;
242
+ const merged = isPlainObject(input.style) ? deepMergeStyle(themed, input.style) : themed;
243
+ return Object.keys(merged).length ? merged : undefined;
244
+ }
245
+ /* ---- aibox'ın komponente özgü uzantısı (aibox-style-logic.ts kopyası) ---- */
246
+ export const HEADER_SECTION_SELECTOR = '& .ng-box-section--header';
247
+ export const FOOTER_SECTION_SELECTOR = '& .ng-box-section--footer';
248
+ const composeAiBoxSections = (input, root) => {
249
+ if (!isPlainObject(input.sectionStyle))
250
+ return;
251
+ const section = input.sectionStyle;
252
+ const color = section.dividerColor || DEFAULT_DIVIDER_COLOR;
253
+ const padding = asDimension(section.sectionPadding);
254
+ const header = {};
255
+ const footer = {};
256
+ // `false` "yok say" değil "ayracı kaldır" demektir.
257
+ if (isSet(section.headerDivider))
258
+ header.borderBottom = section.headerDivider ? `1px solid ${color}` : 'none';
259
+ if (isSet(section.footerDivider))
260
+ footer.borderTop = section.footerDivider ? `1px solid ${color}` : 'none';
261
+ if (padding !== undefined) {
262
+ header.padding = padding;
263
+ footer.padding = padding;
264
+ }
265
+ nest(root, HEADER_SECTION_SELECTOR, header);
266
+ nest(root, FOOTER_SECTION_SELECTOR, footer);
267
+ };
268
+ /* ---- text: emekli prop takma adı (frontend `text-style-logic.ts` kopyası) ---- */
269
+ /** Semantik renk → tema token'ı (`shared/utils.ts` STATUS_COLOR_VAR_MAP kopyası). */
270
+ const STATUS_COLOR_VARS = {
271
+ primary: '--ng-accent-primary',
272
+ info: '--ng-status-info',
273
+ success: '--ng-status-success',
274
+ warning: '--ng-status-warning',
275
+ error: '--ng-status-error'
276
+ };
277
+ const isRenderableColorLike = (value) => typeof value === 'string' && /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\(|var\(--)/i.test(value.trim());
278
+ /**
279
+ * `text`'in EMEKLİ prop'ları (`isBold` / `align` / `color`+`customColor`) kayıtlı
280
+ * sayfalarda aynı CSS'e iner — yalnız tipli alan BOŞSA; tasarımcı
281
+ * `typographyStyle`'da o alanı doldurduysa takma ad susar. Gruplardan sonra,
282
+ * tema birleşiminden önce koşar (tema tabanını ezer, K4).
283
+ */
284
+ export const applyLegacyTextProps = (input, root) => {
285
+ if (root.fontWeight === undefined && input.isBold === true)
286
+ root.fontWeight = '600';
287
+ if (root.textAlign === undefined && isSet(input.align))
288
+ root.textAlign = input.align;
289
+ if (root.color === undefined && isSet(input.color)) {
290
+ const color = String(input.color);
291
+ root.color =
292
+ color === 'custom' && isRenderableColorLike(input.customColor)
293
+ ? String(input.customColor).trim()
294
+ : `var(${STATUS_COLOR_VARS[color] ?? STATUS_COLOR_VARS.primary})`;
295
+ }
296
+ };
297
+ /* ---- container: başlık bölgesi (frontend `container-style-logic.ts` kopyası) ---- */
298
+ const CONTAINER_HEADER_SELECTOR = '&__header';
299
+ const CONTAINER_TITLE_SELECTOR = '&__title';
300
+ /**
301
+ * `headerStyle` container'a özgü gruptur: başlık metninin rengi/puntosu/
302
+ * kalınlığı `&__title` parçasına, ayraç ve bölge dolgusu `&__header`
303
+ * parçasına iner. İkisi de GERÇEK elemandır (SectionHeader onları çizer).
304
+ */
305
+ export const composeContainerHeader = (input, root) => {
306
+ const header = input.headerStyle;
307
+ if (!isPlainObject(header))
308
+ return;
309
+ const region = {};
310
+ const title = {};
311
+ if (isSet(header.divider)) {
312
+ region.borderBottom = header.divider ? `1px solid ${header.dividerColor || DEFAULT_DIVIDER_COLOR}` : 'none';
313
+ }
314
+ else if (isSet(header.dividerColor)) {
315
+ region.borderBottom = `1px solid ${header.dividerColor}`;
316
+ }
317
+ const padding = asDimension(header.padding);
318
+ if (padding !== undefined)
319
+ region.padding = padding;
320
+ const margin = asDimension(header.margin);
321
+ if (margin !== undefined)
322
+ region.margin = margin;
323
+ const minHeight = asDimension(header.minHeight);
324
+ if (minHeight !== undefined)
325
+ region.minHeight = minHeight;
326
+ if (isSet(header.align))
327
+ region.alignItems = header.align;
328
+ if (isSet(header.titleColor))
329
+ title.color = header.titleColor;
330
+ const size = asDimension(header.titleSize);
331
+ if (size !== undefined)
332
+ title.fontSize = size;
333
+ if (isSet(header.titleWeight))
334
+ title.fontWeight = String(header.titleWeight);
335
+ nest(root, CONTAINER_HEADER_SELECTOR, region);
336
+ nest(root, CONTAINER_TITLE_SELECTOR, title);
337
+ };
338
+ /**
339
+ * Tipli stil komponentlerinin kaydı. Yeni bir komponent göçtüğünde buraya bir
340
+ * satır eklenir: hangi grupları taşıdığı, varsa komponente özgü uzantısı ve
341
+ * uzantının okuduğu EMEKLİ prop'lar (`legacyProps` — rapor bunları derleyiciye
342
+ * geçirir ki kayıtlı sayfanın takma adı `resolvedStyle`'da da görünsün).
343
+ * `resolveNodeStyleReport` tamamen bu kayıttan sürülür.
344
+ */
345
+ /**
346
+ * statcard uzantısı — frontend `statcard-style-logic.ts`in aynası.
347
+ * `valueStyle` gerçek eleman sınıfını hedefler; hizalama ÜÇ kurala çevrilir
348
+ * (yalnız `text-align` yazmak kontrolü ölü gösterirdi).
349
+ */
350
+ const STATCARD_FLEX_ALIGN = {
351
+ left: 'flex-start',
352
+ center: 'center',
353
+ right: 'flex-end',
354
+ justify: 'stretch'
355
+ };
356
+ function composeStatCardParts(input, root) {
357
+ const value = input.valueStyle;
358
+ if (isPlainObject(value)) {
359
+ const target = {};
360
+ const v = value;
361
+ if (isSet(v.color))
362
+ target.color = v.color;
363
+ if (isSet(v.fontSize))
364
+ target.fontSize = asDimension(v.fontSize);
365
+ if (isSet(v.fontWeight))
366
+ target.fontWeight = v.fontWeight;
367
+ nest(root, '& .ng-statcard__value', target);
368
+ }
369
+ const typography = isPlainObject(input.typographyStyle)
370
+ ? input.typographyStyle
371
+ : undefined;
372
+ if (typography && isSet(typography.textAlign)) {
373
+ const flex = STATCARD_FLEX_ALIGN[String(typography.textAlign)];
374
+ if (flex) {
375
+ nest(root, '& .ng-statcard__main', { alignItems: flex });
376
+ nest(root, '& .ng-statcard__value-row', { justifyContent: flex });
377
+ }
378
+ }
379
+ }
380
+ export const TYPED_STYLE_COMPONENTS = {
381
+ box: {
382
+ groups: ['surfaceStyle', 'typographyStyle', 'hoverStyle', 'sectionStyle'],
383
+ ext: { overflowTarget: '&__body', extra: composeAiBoxSections }
384
+ },
385
+ // `aibox` EMEKLİ (K5): tipli modeli box devraldı, kayıtlı düğümler box olarak
386
+ // çizilir. Kayıt burada kalır ki o düğümlerin resolvedStyle'ı doğru çıksın.
387
+ aibox: {
388
+ groups: ['surfaceStyle', 'typographyStyle', 'hoverStyle', 'sectionStyle'],
389
+ ext: { overflowTarget: '&__body', extra: composeAiBoxSections }
390
+ },
391
+ container: {
392
+ groups: ['surfaceStyle', 'typographyStyle', 'hoverStyle', 'headerStyle'],
393
+ ext: { overflowTarget: '&__body', extra: composeContainerHeader }
394
+ },
395
+ statcard: {
396
+ groups: ['surfaceStyle', 'typographyStyle', 'hoverStyle', 'valueStyle'],
397
+ ext: { extra: composeStatCardParts },
398
+ // Kartın kaydırılan gövdesi yok; taşma alanı ölü kural üretirdi.
399
+ omit: { surfaceStyle: ['contentOverflow'] }
400
+ },
401
+ text: {
402
+ groups: ['surfaceStyle', 'typographyStyle'],
403
+ ext: { extra: applyLegacyTextProps },
404
+ legacyProps: ['isBold', 'align', 'color', 'customColor'],
405
+ // Taşma hedefi olmayan komponentte contentOverflow ölü kural üretirdi.
406
+ omit: { surfaceStyle: ['contentOverflow'] }
407
+ },
408
+ icon: {
409
+ // Yalnız hover: surface çipin (chipShape/chipColor), typography da boyutun
410
+ // (size) ve rengin (color) çift kanalı olurdu — görsel kararlar semantik
411
+ // prop'larda yaşar (gerekçe komponent şemasında).
412
+ groups: ['hoverStyle'],
413
+ // Kökte kenarlık yok; hover'ın borderColor alanı ölü kural üretirdi.
414
+ omit: { hoverStyle: ['borderColor'] }
415
+ }
416
+ };
417
+ /** text derlemesi — testler ve dış tüketiciler için sarmalayıcı. */
418
+ export function composeTextStyle(input) {
419
+ return composeTypedStyle(input, TYPED_STYLE_COMPONENTS.text.ext);
420
+ }
421
+ /** aibox derlemesi — testler ve dış tüketiciler için sarmalayıcı. */
422
+ export function composeAiBoxStyle(input) {
423
+ return composeTypedStyle(input, TYPED_STYLE_COMPONENTS.aibox.ext);
424
+ }
425
+ /**
426
+ * Prop zarfını HER SEVİYEDE açar.
427
+ *
428
+ * Platform yalnız üst seviyeyi değil, bir prop'un `data`'sının İÇİNİ de
429
+ * `{ type, data }` zarfıyla saklar. Yalnız üst seviyeyi açmak
430
+ * `surfaceStyle.borderColor`'ı ham nesne bırakır ve derleme
431
+ * `1px solid [object Object]` üretir (gerçek sayfada ölçüldü). Frontend'de
432
+ * aynı işi `mapSchemaPropToRawProp` yapar.
433
+ */
434
+ const unwrapProp = (value) => {
435
+ if (Array.isArray(value))
436
+ return value.map(unwrapProp);
437
+ if (!isPlainObject(value))
438
+ return value;
439
+ if ('data' in value && ('type' in value || 'ref' in value)) {
440
+ return unwrapProp(value.data);
441
+ }
442
+ const result = {};
443
+ for (const [key, item] of Object.entries(value))
444
+ result[key] = unwrapProp(item);
445
+ return result;
446
+ };
447
+ const rawProp = (props, name) => {
448
+ const prop = props?.[name];
449
+ // İfadeye bağlı prop'un değeri çalışma anında oluşur, burada çözülemez.
450
+ return prop && typeof prop === 'object' && 'data' in prop ? unwrapProp(prop.data) : undefined;
451
+ };
452
+ /**
453
+ * Bir düğümün efektif stilini ve hangi katmanların katkı verdiğini döner.
454
+ * Tipli komponentte K4 sırası (`composeTypedStyle`), diğerlerinde eski
455
+ * birleşik kaskad uygulanır.
456
+ */
457
+ export function resolveNodeStyleReport(node, source) {
458
+ const props = node.props;
459
+ const ownStyle = rawProp(props, 'style');
460
+ const config = node.type ? TYPED_STYLE_COMPONENTS[node.type] : undefined;
461
+ const presetName = isPlainObject(ownStyle) ? ownStyle[STYLE_PRESET_KEY] : undefined;
462
+ const typedGroups = config ? config.groups.filter(name => isPlainObject(rawProp(props, name))) : [];
463
+ const layers = {
464
+ typedGroups,
465
+ componentStyle: Boolean(node.type && isPlainObject(source?.componentStyles?.[node.type])),
466
+ preset: typeof presetName === 'string' && presetName ? presetName : null,
467
+ own: isPlainObject(ownStyle) ? Object.keys(ownStyle).filter(key => key !== STYLE_PRESET_KEY).length > 0 : Boolean(ownStyle)
468
+ };
469
+ if (!config) {
470
+ // Kayıtsız tip: eski birleşik kaskad.
471
+ const cascaded = resolveNodeStyle(ownStyle, source, node.type);
472
+ return cascaded === undefined ? { styleLayers: layers } : { resolvedStyle: cascaded, styleLayers: layers };
473
+ }
474
+ // Tipli komponent gruplar boşken de tipli yoldan geçer: uzantı emekli
475
+ // prop'ları okuyabilir (text'in isBold/align/color takma adı) ve K4 sırası
476
+ // gruplar boşken birleşik kaskadla aynı sonucu verir.
477
+ const { theme, own } = resolveNodeStyleLayers(ownStyle, source, node.type);
478
+ const input = { themeStyle: theme, style: own };
479
+ for (const name of config.groups)
480
+ input[name] = rawProp(props, name);
481
+ for (const name of config.legacyProps ?? [])
482
+ input[name] = rawProp(props, name);
483
+ const composed = composeTypedStyle(input, config.ext);
484
+ return composed === undefined ? { styleLayers: layers } : { resolvedStyle: composed, styleLayers: layers };
485
+ }
@@ -187,6 +187,10 @@ function describeResult(result) {
187
187
  : '';
188
188
  if (call?.isError)
189
189
  return { ok: false, summary: clip(text, 400) };
190
+ const runtimeErrors = call?.structuredContent?.runtimeErrors;
191
+ if (Array.isArray(runtimeErrors) && runtimeErrors.length > 0) {
192
+ return { ok: false, summary: `çalışma anı hatası (platform HTTP 200 döndü): ${clip(runtimeErrors.map(String).join(' | '), 400)}` };
193
+ }
190
194
  const effect = summarizeEffect(text);
191
195
  return { ok: true, summary: `${Buffer.byteLength(text, 'utf8')} B yanıt${effect ? ` · ${effect}` : ''}` };
192
196
  }
@@ -76,6 +76,9 @@ export class Platform {
76
76
  environment: this.resolveEnvironment(options.environment)
77
77
  }, true);
78
78
  }
79
+ async accessToken() {
80
+ return (await this.authorizationHeader()).replace(/^Bearer\s+/i, '');
81
+ }
79
82
  async authorizationHeader() {
80
83
  if (this.config.authMode === 'passthrough') {
81
84
  const incoming = requestAuthStorage.getStore();
@@ -14,6 +14,75 @@ export function prune(value) {
14
14
  }
15
15
  return value;
16
16
  }
17
+ /**
18
+ * Adımın kimliği bir seviye aşağıdadır: `StepOutputDto`'nun `Name` alanı YOKTUR, ad
19
+ * `steps[].step.name` içinde durur (`WorkflowPreviewDto.cs:40` → `WorkflowStepDto(Guid Id,
20
+ * string Name, StepType Type)`, satır 93). `hasError`/`errorCode`/`errorMessage` ise üst
21
+ * seviyededir (satır 43-45).
22
+ */
23
+ function stepName(node) {
24
+ const nested = node.step;
25
+ if (nested && typeof nested === 'object') {
26
+ const name = nested.name;
27
+ if (typeof name === 'string' && name !== '')
28
+ return name;
29
+ }
30
+ return typeof node.name === 'string' && node.name !== '' ? node.name : '(adsız adım)';
31
+ }
32
+ /**
33
+ * İç içe düğümler `hasError` TAŞIMAZ, hep bir `steps[]` dizisine geri döner:
34
+ * `children` elemanı `ChildStepOutputDto : WorkflowPreviewDto`'dur (`WorkflowPreviewDto.cs:51`
35
+ * + `:110`), yani kendi `hasError`/`children` alanı yok — gerçek yol
36
+ * `steps[].children[].steps[].hasError`. `automations` elemanı da `AutomationNodeDto`'dur ve
37
+ * hatası aynı biçimde `automations[].steps[]` altındadır (`WorkflowPreviewDto.cs:52`, `:135`).
38
+ */
39
+ function collectStepErrors(nodes, into) {
40
+ if (!Array.isArray(nodes))
41
+ return;
42
+ for (const entry of nodes) {
43
+ if (!entry || typeof entry !== 'object')
44
+ continue;
45
+ const node = entry;
46
+ if (node.hasError === true) {
47
+ const code = typeof node.errorCode === 'string' && node.errorCode !== '' ? node.errorCode : 'bilinmeyen kod';
48
+ const message = typeof node.errorMessage === 'string' && node.errorMessage !== '' ? `: ${node.errorMessage}` : '';
49
+ into.push(`${stepName(node)} — ${code}${message}`);
50
+ }
51
+ collectStepErrors(node.steps, into);
52
+ collectStepErrors(node.children, into);
53
+ collectStepErrors(node.automations, into);
54
+ }
55
+ }
56
+ /**
57
+ * Patlayan bir preview/execute HTTP 200 döner; hata gövdenin içinde `steps[].hasError` alanındadır
58
+ * (WorkflowPreviewDto.cs:38-52). Yanıttaki `executionSuccess` tek başına yeterli değil: platform onu
59
+ * `Steps.LastOrDefault()?.HasError is false` ile hesaplar (WorkflowPreviewDto.cs:25), yani ortada
60
+ * patlayıp hata dalından dönen akış `true` üretir. Bu yüzden önce TÜM adımlar taranır.
61
+ *
62
+ * Adım taraması yalnız `WorkflowPreviewDto` döndüren uçları kapsar (workflow/automation preview).
63
+ * Microservice tool'ları `tenantRaw` kullanır ve `http.ts:242` 4xx/5xx'te fırlatmadan
64
+ * `{ httpStatus, body }` döner; `MicroserviceExecutionItem`'da `hasError` de `steps` de yoktur
65
+ * (MicroserviceExecutionItem.cs:3-17), yani HTTP 500 ile biten bir çağrı adım taramasına hiç
66
+ * yakalanmıyordu. Üst seviyedeki `httpStatus` yalnız o raw sarmalından gelir — platform gövdeleri
67
+ * bu adı hiç kullanmaz (DTO'lar `httpStatusCode` yazar), bu yüzden yanlış pozitif üretmez.
68
+ */
69
+ export function runtimeErrorSummary(payload) {
70
+ if (!payload || typeof payload !== 'object')
71
+ return [];
72
+ const record = payload;
73
+ const body = (record.data && typeof record.data === 'object' && !Array.isArray(record.data) ? record.data : record);
74
+ const errors = [];
75
+ if (typeof record.httpStatus === 'number' && record.httpStatus >= 400) {
76
+ 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.`);
77
+ }
78
+ const stepErrors = [];
79
+ collectStepErrors(body.steps, stepErrors);
80
+ if (stepErrors.length === 0 && body.executionSuccess === false) {
81
+ stepErrors.push('executionSuccess: false — akış başarısız bitti, adım listesinde hata işareti yok');
82
+ }
83
+ errors.push(...stepErrors);
84
+ return errors;
85
+ }
17
86
  function findLongestArray(value, best) {
18
87
  if (Array.isArray(value)) {
19
88
  if (!best.array || value.length > best.array.length)
@@ -57,14 +126,21 @@ function asStructured(payload) {
57
126
  return { data: payload };
58
127
  }
59
128
  export function toolResult(payload, config) {
60
- const { payload: bounded, truncated } = shrink(prune(payload), config.maxResponseBytes);
129
+ const pruned = prune(payload);
130
+ const runtimeErrors = runtimeErrorSummary(pruned);
131
+ if (runtimeErrors.length > 0) {
132
+ logStep('akış hatası ✕', `${runtimeErrors.length} adım hata ile bitti — platform HTTP 200 döndürdüğü için çağrı başarılı görünüyordu`, runtimeErrors);
133
+ }
134
+ const { payload: bounded, truncated } = shrink(pruned, config.maxResponseBytes);
61
135
  const structured = asStructured(bounded);
62
136
  if (truncated) {
63
137
  logStep('kırpma ✂', `yanıt ${config.maxResponseBytes} bayt sınırını aştı — en uzun liste yarıya indirilerek küçültüldü (MAX_RESPONSE_BYTES)`);
64
138
  }
65
- const enriched = truncated
66
- ? { ...structured, truncated: true, truncationNote: 'Liste, yanıt boyutu sınırı nedeniyle kırpıldı; sayfalama ya da daha dar filtre kullanın.' }
67
- : structured;
139
+ const enriched = {
140
+ ...structured,
141
+ ...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
142
+ ...(truncated ? { truncated: true, truncationNote: 'Liste, yanıt boyutu sınırı nedeniyle kırpıldı; sayfalama ya da daha dar filtre kullanın.' } : {})
143
+ };
68
144
  return {
69
145
  content: [{ type: 'text', text: JSON.stringify(enriched) }],
70
146
  structuredContent: enriched