@appaflytech/zeyrek-ai-mcp 0.0.23 → 0.0.24
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 +15 -0
- package/dist/config.js +9 -0
- package/dist/index.js +2 -0
- package/dist/knowledge/componentCatalog.js +1 -1
- package/dist/knowledge/mobileComponentCatalog.js +231 -58
- package/dist/knowledge/mobileComponentEvents.js +118 -3
- package/dist/knowledge/normalize.js +16 -6
- package/dist/knowledge/platform/lint.js +110 -3
- package/dist/knowledge/platform/mobileRules.js +64 -8
- package/dist/knowledge/rules.js +56 -7
- package/dist/knowledge/styleResolve.js +49 -3
- package/dist/knowledge/styleResolveNative.js +104 -4
- package/dist/observability/trace.js +1 -1
- package/dist/platform/argumentAliases.js +128 -0
- package/dist/platform/bodyFile.js +103 -0
- package/dist/platform/capture/capture.js +29 -5
- package/dist/platform/capture/console-log.js +80 -0
- package/dist/platform/capture/index.js +1 -0
- package/dist/platform/diagnostics.js +10 -4
- package/dist/platform/http.js +23 -7
- package/dist/platform/responses.js +6 -2
- package/dist/platform/schemaReadMemory.js +60 -0
- package/dist/platform/secrets.js +9 -0
- package/dist/platform/strictArguments.js +68 -12
- package/dist/platform/tokenVault.js +187 -4
- package/dist/resources/guides/expressions.md +1 -1
- package/dist/resources/guides/microservice.md +9 -2
- package/dist/resources/guides/middleware.md +5 -1
- package/dist/resources/guides/mobile.md +150 -14
- package/dist/resources/guides/page.md +146 -39
- package/dist/resources/guides/theme.md +4 -1
- package/dist/resources/guides/workflow.md +7 -1
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/server.js +1 -1
- package/dist/tools/datasources.js +1 -1
- package/dist/tools/designSchemaPatch.js +4 -3
- package/dist/tools/externalComponents.js +1 -1
- package/dist/tools/flows.js +70 -4
- package/dist/tools/layouts.js +5 -5
- package/dist/tools/microservices.js +26 -10
- package/dist/tools/pages.js +192 -36
- package/dist/tools/platformInfo.js +4 -1
- package/dist/tools/rows.js +7 -1
- package/dist/tools/schemaOps.js +1 -1
- package/dist/tools/screenWrite.js +4 -3
- package/dist/tools/screens.js +81 -0
- package/package.json +1 -1
|
@@ -154,6 +154,24 @@ const resolveShadow = (shadow) => {
|
|
|
154
154
|
return undefined;
|
|
155
155
|
return SHADOW_VALUES[shadow] ?? String(shadow);
|
|
156
156
|
};
|
|
157
|
+
/**
|
|
158
|
+
* Döndür / ölçekle / kaydır alanlarının kabul aralığı — frontend `style-groups/transform-fields.ts`
|
|
159
|
+
* `TRANSFORM_RANGES`'in kopyası (altın vakalar kilitler). Aralık dışı ve birimli değer yazılmaz.
|
|
160
|
+
*/
|
|
161
|
+
export const TRANSFORM_RANGES = {
|
|
162
|
+
rotate: [-360, 360],
|
|
163
|
+
scale: [10, 500],
|
|
164
|
+
translateX: [-1000, 1000],
|
|
165
|
+
translateY: [-1000, 1000]
|
|
166
|
+
};
|
|
167
|
+
/** Sayı ya da sayısal metin, aralığın içinde; aksi `undefined` — frontend `transformValue`. */
|
|
168
|
+
export const transformValue = (value, field) => {
|
|
169
|
+
if (typeof value !== 'number' && (typeof value !== 'string' || value.trim() === ''))
|
|
170
|
+
return undefined;
|
|
171
|
+
const parsed = Number(value);
|
|
172
|
+
const [min, max] = TRANSFORM_RANGES[field];
|
|
173
|
+
return Number.isFinite(parsed) && parsed >= min && parsed <= max ? parsed : undefined;
|
|
174
|
+
};
|
|
157
175
|
/**
|
|
158
176
|
* Grupların alan listeleri — frontend `style-groups/style-group-schemas.ts`
|
|
159
177
|
* `STYLE_GROUP_FIELDS`'in kopyası. Parite testi `components.json`'daki gerçek
|
|
@@ -170,7 +188,11 @@ export const STYLE_GROUP_FIELDS = {
|
|
|
170
188
|
'borderLeft',
|
|
171
189
|
'shadow',
|
|
172
190
|
'contentOverflow',
|
|
173
|
-
'opacity'
|
|
191
|
+
'opacity',
|
|
192
|
+
'rotate',
|
|
193
|
+
'scale',
|
|
194
|
+
'translateX',
|
|
195
|
+
'translateY'
|
|
174
196
|
],
|
|
175
197
|
typographyStyle: [
|
|
176
198
|
'textColor',
|
|
@@ -220,6 +242,21 @@ export function composeTypedStyle(input, ext) {
|
|
|
220
242
|
if (opacity !== undefined && opacity >= 0 && opacity < 100) {
|
|
221
243
|
groups.opacity = String(opacity / 100);
|
|
222
244
|
}
|
|
245
|
+
// Döndür / ölçekle / kaydır (ADR 0005): kaydır → döndür → ölçekle, köken merkez;
|
|
246
|
+
// ölçek yüzdedir, nötr değer ve aralık dışı yazılmaz — frontend `composeSurfaceTransform`.
|
|
247
|
+
const transform = [];
|
|
248
|
+
const x = transformValue(surface.translateX, 'translateX') ?? 0;
|
|
249
|
+
const y = transformValue(surface.translateY, 'translateY') ?? 0;
|
|
250
|
+
if (x !== 0 || y !== 0)
|
|
251
|
+
transform.push(`translate(${x}px, ${y}px)`);
|
|
252
|
+
const rotate = transformValue(surface.rotate, 'rotate');
|
|
253
|
+
if (rotate !== undefined && rotate !== 0)
|
|
254
|
+
transform.push(`rotate(${rotate}deg)`);
|
|
255
|
+
const scale = transformValue(surface.scale, 'scale');
|
|
256
|
+
if (scale !== undefined && scale !== 100)
|
|
257
|
+
transform.push(`scale(${scale / 100})`);
|
|
258
|
+
if (transform.length)
|
|
259
|
+
groups.transform = transform.join(' ');
|
|
223
260
|
// `ROOT_TARGET` (`&`) kökün kendisidir: kendi derleyicisi olmayan kaplarda
|
|
224
261
|
// yüzey düğüm kutusuna yazılır ve kaydırılan da odur.
|
|
225
262
|
if (isSet(surface.contentOverflow) && ext?.overflowTarget) {
|
|
@@ -251,8 +288,10 @@ export function composeTypedStyle(input, ext) {
|
|
|
251
288
|
assign(rules, 'borderColor', hover.borderColor);
|
|
252
289
|
assign(rules, 'boxShadow', resolveShadow(hover.shadow));
|
|
253
290
|
const lift = asDimension(hover.lift);
|
|
254
|
-
|
|
255
|
-
|
|
291
|
+
// Kaldırma düğümün kendi transform'unu ezmez, önüne eklenir.
|
|
292
|
+
if (lift !== undefined) {
|
|
293
|
+
rules.transform = `translateY(calc(-1 * ${lift}))${typeof groups.transform === 'string' ? ` ${groups.transform}` : ''}`;
|
|
294
|
+
}
|
|
256
295
|
if (Object.keys(rules).length) {
|
|
257
296
|
const duration = asNumber(hover.transitionMs) ?? 160;
|
|
258
297
|
groups.transition = `background-color ${duration}ms ease, border-color ${duration}ms ease, box-shadow ${duration}ms ease, transform ${duration}ms ease`;
|
|
@@ -500,6 +539,13 @@ export const NODE_SURFACE_TYPES = new Set([
|
|
|
500
539
|
'qm-sidebar',
|
|
501
540
|
'auth-layout'
|
|
502
541
|
]);
|
|
542
|
+
/**
|
|
543
|
+
* `style`'ı kendi içeriğinin CSS'i olarak alan tipler: `html`in `style`'ı uzun metindir ve yazarın
|
|
544
|
+
* işaretlemesine kapsamlı girer — web'de emotion sınıfı (`html.functions.ts` `resolveHtmlStyleClass`),
|
|
545
|
+
* mobilde korumalı belgenin stil bloğu (plans/mobil-plantcare-acigi f-20). "Düz CSS string style"
|
|
546
|
+
* (PAGE-014) ve "ham CSS çizilmez" (MOB-002) uyarıları bu tiplerde yanlıştır.
|
|
547
|
+
*/
|
|
548
|
+
export const DOCUMENT_STYLE_TYPES = new Set(['html']);
|
|
503
549
|
const NODE_SURFACE_EXTENSION = { overflowTarget: ROOT_TARGET };
|
|
504
550
|
/** Düğüm kutusuna inen yüzey; kap tipi değilse ya da grup boşsa yok. */
|
|
505
551
|
export function composeNodeSurfaceStyle(type, surfaceStyle) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { rawProp } from './styleResolve.js';
|
|
1
|
+
import { TRANSFORM_RANGES, rawProp, transformValue } from './styleResolve.js';
|
|
2
2
|
import { mergeTypeScale, resolveTheme } from './themeResolve.js';
|
|
3
3
|
/* ---- css-value.ts ---- */
|
|
4
4
|
const warn = (context, message) => {
|
|
@@ -45,6 +45,46 @@ export function parseFontFamily(value) {
|
|
|
45
45
|
const first = String(value).split(',')[0].trim().replace(/^["']|["']$/g, '');
|
|
46
46
|
return first || null;
|
|
47
47
|
}
|
|
48
|
+
const GENERIC_FONT_FAMILIES = new Set([
|
|
49
|
+
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
|
|
50
|
+
'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong'
|
|
51
|
+
]);
|
|
52
|
+
/** CSS generik ailesi mi (`sans-serif`, `monospace`…) — tırnaklanırsa aile adı sanılır. */
|
|
53
|
+
export function isGenericFontFamily(name) {
|
|
54
|
+
return GENERIC_FONT_FAMILIES.has(name.trim().toLowerCase());
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Yığının yedek generiği: son generik ad, yoksa `sans-serif`. Temanın yığını `"Figtree",
|
|
58
|
+
* sans-serif` biçimindedir; tek ad yazılan `typographyStyle` yedeğini buradan miras alır (ZM-10).
|
|
59
|
+
*/
|
|
60
|
+
export function fontFallback(stack) {
|
|
61
|
+
const generic = String(stack ?? '').split(',').map(part => part.trim().replace(/^["']|["']$/g, '')).filter(isGenericFontFamily).pop();
|
|
62
|
+
return generic ?? 'sans-serif';
|
|
63
|
+
}
|
|
64
|
+
/** Yığının yalın ilk adı (tırnaksız); yükleyicinin istediği ad budur (ZM-10, f-05). */
|
|
65
|
+
export function firstFontFamily(value) {
|
|
66
|
+
return parseFontFamily(value) ?? '';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Font adı platforma göre çözülür (ZM-10, K3). Frontend'de bu WEB/cihaz diye ikiye ayrılır
|
|
70
|
+
* (`font-family.ts` / `font-family.native.ts`, Metro `.native`'i seçer) ama bu ayna TEK koldur:
|
|
71
|
+
* `test/style-resolve-native.test.ts` frontend'in `theme-native.ts`'ini düz Node `import()` ile
|
|
72
|
+
* okur ve Node/Vite çözümü platform uzantısını (`.native.ts`) hiç görmez — parite testinin
|
|
73
|
+
* kıyasladığı gerçek davranış bu yüzden HER ZAMAN web kolu (`font-family.ts`); cihazdaki yalın-ad
|
|
74
|
+
* davranışı yalnız Metro bundle'ında oluşur ve bu aynanın kapsamı dışındadır (g-02 parity fix).
|
|
75
|
+
* Tek ad generik yedeksiz kalırsa RN web'de font yüklenemediğinde tarayıcı varsayılanı (Times)
|
|
76
|
+
* çizilir; bu yüzden tek ad tırnaklanıp temanın generiğiyle yığına çevrilir, yazarın kendi yığını
|
|
77
|
+
* (virgüllü) aynen geçer.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveFontFamily(value, fallback) {
|
|
80
|
+
const text = value.trim();
|
|
81
|
+
if (text === '' || text.includes(','))
|
|
82
|
+
return text;
|
|
83
|
+
const name = firstFontFamily(text);
|
|
84
|
+
if (isGenericFontFamily(name))
|
|
85
|
+
return name;
|
|
86
|
+
return `"${name}", ${fallback}`;
|
|
87
|
+
}
|
|
48
88
|
export function parseLineHeight(value, fontSize, context) {
|
|
49
89
|
if (value === undefined || value === null || value === '')
|
|
50
90
|
return null;
|
|
@@ -202,6 +242,9 @@ export function resolveThemeNative(theme, mode = 'light') {
|
|
|
202
242
|
if (narrow)
|
|
203
243
|
scale[`${level}Mobile`] = px(narrow, fontSize);
|
|
204
244
|
}
|
|
245
|
+
const bodyStack = vars['--ng-font'] ?? tokens.fontFamily;
|
|
246
|
+
const headingStack = vars['--ng-font-heading'] ?? bodyStack;
|
|
247
|
+
const fallback = fontFallback(bodyStack);
|
|
205
248
|
return {
|
|
206
249
|
mode,
|
|
207
250
|
vars,
|
|
@@ -214,8 +257,11 @@ export function resolveThemeNative(theme, mode = 'light') {
|
|
|
214
257
|
pill: px(vars['--ng-radius-pill'], 999)
|
|
215
258
|
},
|
|
216
259
|
font: {
|
|
217
|
-
family:
|
|
218
|
-
headingFamily:
|
|
260
|
+
family: resolveFontFamily(bodyStack, fallback),
|
|
261
|
+
headingFamily: resolveFontFamily(headingStack, fallback),
|
|
262
|
+
source: firstFontFamily(bodyStack),
|
|
263
|
+
headingSource: firstFontFamily(headingStack),
|
|
264
|
+
fallback,
|
|
219
265
|
size: fontSize,
|
|
220
266
|
weight: vars['--ng-font-weight'] ?? tokens.fontWeight,
|
|
221
267
|
scale
|
|
@@ -302,6 +348,55 @@ const composeBorders = (surface, shell, theme, context) => {
|
|
|
302
348
|
shell.borderStyle = style;
|
|
303
349
|
}
|
|
304
350
|
};
|
|
351
|
+
/** Yüzey grubunun bilinen alanları; kalanı uyarıya yazılır (frontend `SURFACE_KEYS`). MOB-002 de buradan okur. */
|
|
352
|
+
export const SURFACE_KEYS = new Set([
|
|
353
|
+
'background',
|
|
354
|
+
'radius',
|
|
355
|
+
'borderTop',
|
|
356
|
+
'borderRight',
|
|
357
|
+
'borderBottom',
|
|
358
|
+
'borderLeft',
|
|
359
|
+
'shadow',
|
|
360
|
+
'contentOverflow',
|
|
361
|
+
'opacity',
|
|
362
|
+
'rotate',
|
|
363
|
+
'scale',
|
|
364
|
+
'translateX',
|
|
365
|
+
'translateY',
|
|
366
|
+
'borderWidth',
|
|
367
|
+
'borderStyle',
|
|
368
|
+
'borderColor'
|
|
369
|
+
]);
|
|
370
|
+
/**
|
|
371
|
+
* Döndür / ölçekle / kaydır (ADR 0005): web ile aynı sıra ve aralık, RN dizisi. Dolu ama geçersiz
|
|
372
|
+
* değer (birimli metin, aralık dışı) uyarıya yazılır — frontend `composeTransform`.
|
|
373
|
+
*/
|
|
374
|
+
const composeTransform = (surface, shell, warnings) => {
|
|
375
|
+
const read = (field) => {
|
|
376
|
+
const raw = surface[field];
|
|
377
|
+
if (!isSet(raw))
|
|
378
|
+
return undefined;
|
|
379
|
+
const value = transformValue(raw, field);
|
|
380
|
+
if (value === undefined) {
|
|
381
|
+
const [min, max] = TRANSFORM_RANGES[field];
|
|
382
|
+
warnings?.push(`surfaceStyle.${field}: ${String(raw)} geçersiz — ${min}…${max} arası sayı, atlandı`);
|
|
383
|
+
}
|
|
384
|
+
return value;
|
|
385
|
+
};
|
|
386
|
+
const transform = [];
|
|
387
|
+
const x = read('translateX') ?? 0;
|
|
388
|
+
const y = read('translateY') ?? 0;
|
|
389
|
+
if (x !== 0 || y !== 0)
|
|
390
|
+
transform.push({ translateX: x }, { translateY: y });
|
|
391
|
+
const rotate = read('rotate');
|
|
392
|
+
if (rotate !== undefined && rotate !== 0)
|
|
393
|
+
transform.push({ rotate: `${rotate}deg` });
|
|
394
|
+
const scale = read('scale');
|
|
395
|
+
if (scale !== undefined && scale !== 100)
|
|
396
|
+
transform.push({ scale: scale / 100 });
|
|
397
|
+
if (transform.length)
|
|
398
|
+
shell.transform = transform;
|
|
399
|
+
};
|
|
305
400
|
const composeSurface = (surface, shell, theme, context) => {
|
|
306
401
|
const background = asText(surface.background, theme);
|
|
307
402
|
if (background)
|
|
@@ -325,12 +420,17 @@ const composeSurface = (surface, shell, theme, context) => {
|
|
|
325
420
|
if (overflow === 'auto')
|
|
326
421
|
context.warnings?.push('contentOverflow: auto — RN kaydırmayı bileşenin kendisi açar, kutu hidden yapıldı');
|
|
327
422
|
}
|
|
423
|
+
composeTransform(surface, shell, context.warnings);
|
|
424
|
+
for (const [key, value] of Object.entries(surface)) {
|
|
425
|
+
if (!SURFACE_KEYS.has(key) && isSet(value))
|
|
426
|
+
context.warnings?.push(`surfaceStyle.${key}: mobilde karşılığı yok, atlandı`);
|
|
427
|
+
}
|
|
328
428
|
};
|
|
329
429
|
const composeTypography = (typography, text, theme, context) => {
|
|
330
430
|
const color = asText(typography.textColor, theme);
|
|
331
431
|
if (color)
|
|
332
432
|
text.color = color;
|
|
333
|
-
const family =
|
|
433
|
+
const family = resolveFontFamily(asText(typography.fontFamily, theme) ?? '', theme.font.fallback);
|
|
334
434
|
if (family)
|
|
335
435
|
text.fontFamily = family;
|
|
336
436
|
const fontSize = parsePanelLength(typeof typography.fontSize === 'string' ? resolveThemeToken(typography.fontSize, theme) : typography.fontSize, context);
|
|
@@ -351,7 +351,7 @@ export function logGatewayEvent(message) {
|
|
|
351
351
|
*/
|
|
352
352
|
export function logCapabilityGap(gap) {
|
|
353
353
|
const lines = [
|
|
354
|
-
`┌─ ${clockOf(new Date())} · KABİLİYET BOŞLUĞU · ${gap.severity}${gap.module ? ` · ${gap.module}` : ''}`,
|
|
354
|
+
`┌─ ${clockOf(new Date())} · KABİLİYET BOŞLUĞU · ${gap.severity}${gap.module ? ` · ${gap.module}` : ''}${gap.project ? ` · proje ${gap.project}` : ''}`,
|
|
355
355
|
`│ hedef : ${condense(gap.goal, maxBodyChars)}`,
|
|
356
356
|
`│ engel : ${condense(gap.blocker, maxBodyChars)}`
|
|
357
357
|
];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { PlatformApiError } from './errors.js';
|
|
3
|
+
import { RefResolver, isGuid } from './refs.js';
|
|
4
|
+
const text = (value) => typeof value === 'string' ? value : String(value ?? '');
|
|
5
|
+
const hostAlias = (from, host) => ({
|
|
6
|
+
tool: '*',
|
|
7
|
+
from,
|
|
8
|
+
when: (shape, args) => 'host' in shape && 'hostId' in shape && !(from in shape) && args.host === undefined && args.hostId === undefined,
|
|
9
|
+
apply: async (value) => ({ set: { host, hostId: value }, note: `${from} → host: '${host}', hostId: ${text(value)}` })
|
|
10
|
+
});
|
|
11
|
+
export const ARGUMENT_ALIASES = [
|
|
12
|
+
hostAlias('pageId', 'page'),
|
|
13
|
+
hostAlias('layoutId', 'layout'),
|
|
14
|
+
hostAlias('componentId', 'component'),
|
|
15
|
+
{
|
|
16
|
+
tool: '*',
|
|
17
|
+
from: 'project',
|
|
18
|
+
when: (shape, args) => 'projectId' in shape && !('project' in shape) && args.projectId === undefined,
|
|
19
|
+
apply: async (value, _args, { platform }) => {
|
|
20
|
+
if (isGuid(text(value)))
|
|
21
|
+
return { set: { projectId: text(value) }, note: `project → projectId: ${text(value)}` };
|
|
22
|
+
// Önce slug (`/p/{slug}`), sonra ad: liste arama sonucunda slug ya da ad birebir (büyük/küçük harf duyarsız).
|
|
23
|
+
const bySlug = await platform.org('GET', `/p/${encodeURIComponent(text(value))}`).catch(() => undefined);
|
|
24
|
+
const wanted = text(value).toLocaleLowerCase('tr');
|
|
25
|
+
const listed = bySlug?.data?.id
|
|
26
|
+
? undefined
|
|
27
|
+
: await platform.org('GET', '/projects', { query: { searchTerm: text(value), pageLength: 50 } }).catch(() => undefined);
|
|
28
|
+
const projectId = bySlug?.data?.id ?? listed?.data?.find(project => [project.slug, project.name].some(candidate => candidate?.toLocaleLowerCase('tr') === wanted))?.id;
|
|
29
|
+
if (!projectId)
|
|
30
|
+
throw new PlatformApiError(`'${text(value)}' diye bir proje yok (slug ya da ad); projectId'yi project_list ile bulun.`, 400);
|
|
31
|
+
return { set: { projectId }, note: `project '${text(value)}' → projectId: ${projectId}` };
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
tool: '*',
|
|
36
|
+
from: 'id',
|
|
37
|
+
when: (shape, args) => 'microservice' in shape && 'application' in shape && !('id' in shape) && args.microservice === undefined,
|
|
38
|
+
apply: async (value, args, { platform }) => {
|
|
39
|
+
if (!isGuid(text(value)))
|
|
40
|
+
return { set: { microservice: text(value) }, note: `id → microservice: '${text(value)}'` };
|
|
41
|
+
if (typeof args.application !== 'string')
|
|
42
|
+
throw new PlatformApiError("id bir microservice GUID'i; slug'ını çözmek için 'application' (uygulama slug'ı) da verin.", 400);
|
|
43
|
+
const resolver = new RefResolver(platform, args.project, args.environment);
|
|
44
|
+
const applicationId = await resolver.applicationId(args.application);
|
|
45
|
+
const payload = await platform.tenant('GET', args.project, `/microservices/${applicationId}/${text(value)}`, { environment: args.environment });
|
|
46
|
+
const slug = payload?.data?.slug;
|
|
47
|
+
if (!slug)
|
|
48
|
+
throw new PlatformApiError(`'${args.application}' uygulamasında ${text(value)} id'li microservice yok; microservice_list ile slug'ı bulun.`, 400);
|
|
49
|
+
return { set: { microservice: slug }, note: `id ${text(value)} → microservice: '${slug}'` };
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
tool: 'entity_get',
|
|
54
|
+
from: 'applicationId',
|
|
55
|
+
apply: async (value, args, { platform }) => {
|
|
56
|
+
const project = args.project;
|
|
57
|
+
const environment = args.environment;
|
|
58
|
+
const resolver = new RefResolver(platform, project, environment);
|
|
59
|
+
const applicationId = await resolver.applicationId(text(value));
|
|
60
|
+
const entity = await resolver.entity(text(args.id));
|
|
61
|
+
const payload = await platform.tenant('GET', project, `/entities/${entity.datasourceId}/${entity.entityId}`, { environment });
|
|
62
|
+
const linked = (payload?.data?.applications ?? []).some(link => [link.id, link.applicationId].some(id => id?.toLowerCase() === applicationId.toLowerCase()));
|
|
63
|
+
if (!linked) {
|
|
64
|
+
throw new PlatformApiError(`'${text(args.id)}' entity'si '${text(value)}' uygulamasına bağlı değil; entity_get uygulamaya göre değil entity'ye göre çalışır — applicationId'yi kaldırın ya da uygulamanın entity'lerini entity_list {applicationId} ile listeleyin.`, 400);
|
|
65
|
+
}
|
|
66
|
+
return { set: { datasourceId: entity.datasourceId }, note: `applicationId '${text(value)}' → datasourceId: ${entity.datasourceId} (entity '${text(args.id)}' bu uygulamaya bağlı)` };
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
tool: '*',
|
|
71
|
+
from: 'applicationId',
|
|
72
|
+
when: (shape, args) => shape.applicationId !== undefined && typeof args.applicationId === 'string' && !shape.applicationId.safeParse(args.applicationId).success,
|
|
73
|
+
apply: async (value, args, { platform }) => {
|
|
74
|
+
const applicationId = await new RefResolver(platform, args.project, args.environment).applicationId(text(value));
|
|
75
|
+
return { set: { applicationId }, note: `applicationId '${text(value)}' → ${applicationId}` };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
export async function applyArgumentAliases(tool, args, context) {
|
|
80
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
81
|
+
return { args, applied: [] };
|
|
82
|
+
let current = { ...args };
|
|
83
|
+
const applied = [];
|
|
84
|
+
for (const alias of ARGUMENT_ALIASES) {
|
|
85
|
+
if (alias.tool !== '*' && alias.tool !== tool)
|
|
86
|
+
continue;
|
|
87
|
+
if (!(alias.from in current))
|
|
88
|
+
continue;
|
|
89
|
+
if (!(alias.when ? alias.when(context.shape, current) : !(alias.from in context.shape)))
|
|
90
|
+
continue;
|
|
91
|
+
const outcome = await alias.apply(current[alias.from], current, context);
|
|
92
|
+
const { [alias.from]: _replaced, ...rest } = current;
|
|
93
|
+
current = { ...rest, ...outcome.set };
|
|
94
|
+
applied.push(outcome.note);
|
|
95
|
+
}
|
|
96
|
+
return { args: current, applied };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Takma adı olmayan bilinmeyen argüman için "bunu mu demek istedin": okunan alanlar arasından büyük/küçük
|
|
100
|
+
* harf duyarsız düzenleme uzaklığı 2'yi geçmeyen en yakın ad (`hostID` → `hostId`, `views` → `view`).
|
|
101
|
+
*/
|
|
102
|
+
export function closestArgument(key, accepted) {
|
|
103
|
+
const distance = (left, right) => {
|
|
104
|
+
const row = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
105
|
+
for (let i = 1; i <= left.length; i++) {
|
|
106
|
+
let diagonal = row[0] ?? 0;
|
|
107
|
+
row[0] = i;
|
|
108
|
+
for (let j = 1; j <= right.length; j++) {
|
|
109
|
+
const above = row[j] ?? 0;
|
|
110
|
+
row[j] = Math.min(above + 1, (row[j - 1] ?? 0) + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1));
|
|
111
|
+
diagonal = above;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return row[right.length] ?? 0;
|
|
115
|
+
};
|
|
116
|
+
const ranked = accepted
|
|
117
|
+
.map(name => ({ name, score: distance(key.toLowerCase(), name.toLowerCase()) }))
|
|
118
|
+
.filter(entry => entry.score <= 2)
|
|
119
|
+
.sort((left, right) => left.score - right.score);
|
|
120
|
+
return ranked[0]?.name;
|
|
121
|
+
}
|
|
122
|
+
const activeNotes = new AsyncLocalStorage();
|
|
123
|
+
export function runWithArgumentNotes(notes, run) {
|
|
124
|
+
return activeNotes.run(notes, run);
|
|
125
|
+
}
|
|
126
|
+
export function argumentNotes() {
|
|
127
|
+
return activeNotes.getStore() ?? { aliasesApplied: [] };
|
|
128
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { open, realpath } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { extname, resolve, sep } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { PlatformApiError } from './errors.js';
|
|
6
|
+
/**
|
|
7
|
+
* g-08 (`plans/mobil-plantcare-acigi`): tanım yazan araçlar gövdeyi dosyadan alabilir. Koşuda en
|
|
8
|
+
* büyük bağlam tasarrufu, ajanın kendi yazdığı "dosyadan MCP'ye" betiğinden geldi; o iş buraya
|
|
9
|
+
* taşındı. Her aracın TEK gövde argümanı vardır ve dosyanın JSON'u o argümanın değeri olur. Dosya
|
|
10
|
+
* SDK doğrulamasından ÖNCE okunur (`strictArguments.ts`), yani satır içi gövdeyle aynı şema ve aynı
|
|
11
|
+
* yol — dosyadan gelen gövde hiçbir kontrolü atlamaz.
|
|
12
|
+
*/
|
|
13
|
+
export const BODY_FILE_ARGUMENTS = {
|
|
14
|
+
page_create: 'schema',
|
|
15
|
+
page_update: 'schema',
|
|
16
|
+
page_schema_patch: 'ops',
|
|
17
|
+
flow_create: 'steps',
|
|
18
|
+
flow_update: 'steps',
|
|
19
|
+
flow_patch: 'operations',
|
|
20
|
+
microservice_create: 'definition',
|
|
21
|
+
microservice_update: 'definition',
|
|
22
|
+
microservice_patch: 'operations',
|
|
23
|
+
workflow_create: 'definition',
|
|
24
|
+
workflow_update: 'definition',
|
|
25
|
+
workflow_patch: 'operations'
|
|
26
|
+
};
|
|
27
|
+
/** `ops`/`operations` eşanlamlıdır (`microOps`); ikisinden biri satır içinde varsa gövde verilmiştir. */
|
|
28
|
+
const synonyms = { ops: ['ops', 'operations'], operations: ['ops', 'operations'] };
|
|
29
|
+
const maxBodyFileBytes = 5 * 1024 * 1024;
|
|
30
|
+
export function bodyFileParam(argument) {
|
|
31
|
+
return z.string().min(1).optional().describe(`'${argument}' yerine okunacak yerel .json dosyası (en çok 5 MB): gövde ajan bağlamından geçmez, gateway dosyayı okuyup '${argument}' olarak aynı doğrulamadan geçirir. '${argument}' ile birlikte verilemez. Yol gateway sürecinin çalışma dizininin ya da MCP_INPUT_DIR'in altında olmalı (sembolik bağ çözülür). Yanıtın bodyFile alanı hangi dosyanın okunduğunu söyler.`);
|
|
32
|
+
}
|
|
33
|
+
export async function loadBodyFile(tool, args, config) {
|
|
34
|
+
const argument = BODY_FILE_ARGUMENTS[tool];
|
|
35
|
+
if (!argument || !args || typeof args !== 'object' || Array.isArray(args))
|
|
36
|
+
return { args };
|
|
37
|
+
const { bodyFile, ...rest } = args;
|
|
38
|
+
if (bodyFile === undefined)
|
|
39
|
+
return { args };
|
|
40
|
+
if (typeof bodyFile !== 'string' || bodyFile.length === 0)
|
|
41
|
+
return { args };
|
|
42
|
+
const inline = (synonyms[argument] ?? [argument]).filter(name => rest[name] !== undefined);
|
|
43
|
+
if (inline.length > 0) {
|
|
44
|
+
throw new PlatformApiError(`${tool}: bodyFile ile '${inline.join("', '")}' birlikte verilemez — gövde ya dosyadan ya satır içinden gelir. Birini kaldırın. İstek gönderilmedi.`, 400);
|
|
45
|
+
}
|
|
46
|
+
const absolute = resolve(bodyFile);
|
|
47
|
+
if (extname(absolute).toLowerCase() !== '.json') {
|
|
48
|
+
throw new PlatformApiError(`${tool}: bodyFile yalnız .json okur — verilen: ${absolute}. İstek gönderilmedi.`, 400);
|
|
49
|
+
}
|
|
50
|
+
let real;
|
|
51
|
+
try {
|
|
52
|
+
real = await realpath(absolute);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw new PlatformApiError(`${tool}: bodyFile okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`, 400);
|
|
56
|
+
}
|
|
57
|
+
if (extname(real).toLowerCase() !== '.json') {
|
|
58
|
+
throw new PlatformApiError(`${tool}: bodyFile'ın hedefi .json değil — ${real} (${absolute} bağının hedefi). İstek gönderilmedi.`, 400);
|
|
59
|
+
}
|
|
60
|
+
const roots = await Promise.all([process.cwd(), config.inputDirectory]
|
|
61
|
+
.filter((root) => typeof root === 'string' && root.length > 0)
|
|
62
|
+
.map(root => realpath(resolve(root)).catch(() => undefined)));
|
|
63
|
+
const allowed = roots.some(root => root !== undefined && (real === root || real.startsWith(`${root}${sep}`)));
|
|
64
|
+
if (!allowed) {
|
|
65
|
+
throw new PlatformApiError(`${tool}: bodyFile izinli dizinlerin dışında — ${real}${real !== absolute ? ` (${absolute} bağının hedefi)` : ''}. Gateway yalnız çalışma dizininin (${process.cwd()}) ve .env'deki MCP_INPUT_DIR'in (${config.inputDirectory ?? 'tanımsız'}) altındaki dosyaları okur. İstek gönderilmedi.`, 400);
|
|
66
|
+
}
|
|
67
|
+
// Denetim ve okuma AYNI tutamaçtan: yol denetimden sonra değişse de okunan, denetlenen dosyadır.
|
|
68
|
+
// O_NOFOLLOW son bileşenin sonradan bağa çevrilmesini, O_NONBLOCK `.json` adlı FIFO'nun çağrıyı
|
|
69
|
+
// bekletmesini önler; boyut dosyanın büyüklüğüne değil okunan bayta bakar.
|
|
70
|
+
let handle;
|
|
71
|
+
try {
|
|
72
|
+
handle = await open(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw new PlatformApiError(`${tool}: bodyFile açılamadı — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
|
|
76
|
+
}
|
|
77
|
+
let bytes;
|
|
78
|
+
let text;
|
|
79
|
+
try {
|
|
80
|
+
const info = await handle.stat();
|
|
81
|
+
if (!info.isFile()) {
|
|
82
|
+
throw new PlatformApiError(`${tool}: bodyFile düzenli dosya değil — ${real}. İstek gönderilmedi.`, 400);
|
|
83
|
+
}
|
|
84
|
+
const buffer = Buffer.alloc(maxBodyFileBytes + 1);
|
|
85
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
86
|
+
bytes = bytesRead;
|
|
87
|
+
if (bytes > maxBodyFileBytes) {
|
|
88
|
+
throw new PlatformApiError(`${tool}: bodyFile 5 MB sınırını aşıyor. Tanımı parçalara bölün — sayfa için page_schema_patch'e birden çok batch, akış için flow_patch. İstek gönderilmedi.`, 400);
|
|
89
|
+
}
|
|
90
|
+
text = buffer.subarray(0, bytes).toString('utf8');
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await handle.close();
|
|
94
|
+
}
|
|
95
|
+
let value;
|
|
96
|
+
try {
|
|
97
|
+
value = JSON.parse(text);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new PlatformApiError(`${tool}: bodyFile JSON değil — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
|
|
101
|
+
}
|
|
102
|
+
return { args: { ...rest, [argument]: value }, note: `${bodyFile} → ${argument} (${bytes} B)` };
|
|
103
|
+
}
|
|
@@ -4,6 +4,7 @@ import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../../knowledge/com
|
|
|
4
4
|
import { logStep } from '../../observability/trace.js';
|
|
5
5
|
import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
|
|
6
6
|
import { domSummaryScript } from './summary.js';
|
|
7
|
+
import { createPageConsoleLog, describePageFailure, parseRenderErrors, renderErrorsScript } from './console-log.js';
|
|
7
8
|
import { decideReadiness, describeUnready, parseReadiness, readinessScript, runtimePath } from './readiness.js';
|
|
8
9
|
/**
|
|
9
10
|
* Ziyaretçinin tema tercihinin yaşadığı localStorage anahtarı — `theme-toggle` bunu yazar,
|
|
@@ -17,6 +18,12 @@ const VIEWER_MODE_STORAGE_KEY = 'zeyrek-viewer-theme-mode';
|
|
|
17
18
|
* büyüterek alınır: Studio runtime'ı kendi kaydırma kabını `100vh` üzerine kuruyor ve viewport
|
|
18
19
|
* ötesi yakalama o kabı bir kez daha çiziyordu — aynı sayfa karede iki kez görünüyordu (09-04
|
|
19
20
|
* Y-23). Tek karede çekilen görüntüde o birleştirme adımı hiç yok.
|
|
21
|
+
*
|
|
22
|
+
* İç kaydırıcı için gereken viewport `innerHeight + (scrollHeight − clientHeight)`'tir, kabın
|
|
23
|
+
* `scrollHeight`'i DEĞİL: kap viewport'la birlikte büyür ama üstündeki/altındaki sabit parçalar
|
|
24
|
+
* (mobil host'ta app bar + tab bar, ZM-23 f-06) büyümez. Yalnız `scrollHeight` itilince viewport
|
|
25
|
+
* içerik boyuna çıkıyor, kap hâlâ o parçalar kadar kısa kalıyor ve uzun mobil sayfanın altı
|
|
26
|
+
* karenin dışında kalıyordu. Kap viewport kadar yüksekse iki formül aynı sonucu verir.
|
|
20
27
|
*/
|
|
21
28
|
export const contentHeightScript = [
|
|
22
29
|
'(function () {',
|
|
@@ -27,7 +34,7 @@ export const contentHeightScript = [
|
|
|
27
34
|
' var nodes = document.querySelectorAll("*");',
|
|
28
35
|
' for (var index = 0; index < nodes.length && index < 4000; index += 1) {',
|
|
29
36
|
' var node = nodes[index];',
|
|
30
|
-
' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(node.scrollHeight);',
|
|
37
|
+
' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(viewportHeight + node.scrollHeight - node.clientHeight);',
|
|
31
38
|
' }',
|
|
32
39
|
' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
|
|
33
40
|
'})()'
|
|
@@ -91,6 +98,10 @@ export const layoutReportScript = [
|
|
|
91
98
|
' };',
|
|
92
99
|
'})'
|
|
93
100
|
].join('\n');
|
|
101
|
+
/** Mobil çizicinin hata sınırı işaretleri; betik düşerse boş (çekimi düşürmez). */
|
|
102
|
+
async function readRenderErrors(page) {
|
|
103
|
+
return page.evaluate(renderErrorsScript).then(parseRenderErrors).catch(() => []);
|
|
104
|
+
}
|
|
94
105
|
/**
|
|
95
106
|
* F-09 — sayfa akışı yalnız tarayıcıda koşar, bu yüzden bir setter iteratee'sinin
|
|
96
107
|
* patlamasının gateway'e ulaşan HİÇBİR yolu yoktu: akış 200 dönüyor, hedef prop
|
|
@@ -264,7 +275,7 @@ export class StudioScreenshotter {
|
|
|
264
275
|
* işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
|
|
265
276
|
* yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
|
|
266
277
|
*/
|
|
267
|
-
async runActions(page, request) {
|
|
278
|
+
async runActions(page, request, consoleLog) {
|
|
268
279
|
if (!request.actions || request.actions.length === 0)
|
|
269
280
|
return undefined;
|
|
270
281
|
const outcomes = [];
|
|
@@ -306,7 +317,8 @@ export class StudioScreenshotter {
|
|
|
306
317
|
completedBefore: activityBefore?.completed ?? Number.POSITIVE_INFINITY
|
|
307
318
|
});
|
|
308
319
|
if (readiness.state === 'not-ready' && request.captureUnready !== true) {
|
|
309
|
-
|
|
320
|
+
const failure = consoleLog ? describePageFailure(consoleLog, await readRenderErrors(page)) : '';
|
|
321
|
+
throw new PlatformApiError(`Etkileşim adımı ${index} (${action.action}${action.target === undefined ? '' : ` '${action.target}'`}) sonrasında sayfa ${readiness.waitedMs} ms içinde durulmadı, kare ALINMADI — ${describeUnready(readiness)}.${failure} Bekleme bütçesi ${apiResponseTimeoutMs + request.delayMs} ms; akış gerçekten uzun sürüyorsa delayMs ile büyütün. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
|
|
310
322
|
}
|
|
311
323
|
const after = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => undefined);
|
|
312
324
|
const requests = typeof after === 'number' && typeof before.requests === 'number' ? Math.max(after - before.requests, 0) : 0;
|
|
@@ -459,6 +471,11 @@ export class StudioScreenshotter {
|
|
|
459
471
|
await context.addInitScript(`try { localStorage.setItem(${JSON.stringify(VIEWER_MODE_STORAGE_KEY)}, ${JSON.stringify(request.colorScheme)}) } catch (error) { /* localStorage kapalıysa proje varsayılanı çizilir */ }`);
|
|
460
472
|
}
|
|
461
473
|
const page = await context.newPage();
|
|
474
|
+
// Konsol ve istisna dinleyicileri navigasyondan ÖNCE takılır: modül yüklenirken patlayan
|
|
475
|
+
// bileşenin hatası ilk karede düşer ve sonradan takılan dinleyici onu kaçırırdı (g-06).
|
|
476
|
+
const consoleLog = createPageConsoleLog();
|
|
477
|
+
page.on?.('console', message => consoleLog.onConsole(message));
|
|
478
|
+
page.on?.('pageerror', error => consoleLog.onPageError(error));
|
|
462
479
|
// Soğuk Vite derlemesinde sayfanın kendi veri isteği 15 sn'yi aşabiliyor; delayMs bu
|
|
463
480
|
// bütçeyi de uzatır (09-02 E-2). Yanıt hazırlık döngüsünü BLOKLAMAZ: runtime dışına
|
|
464
481
|
// yönlendirilen sayfa bu isteği hiç atmaz ve eskiden 15 sn boşa bekleniyordu.
|
|
@@ -499,7 +516,8 @@ export class StudioScreenshotter {
|
|
|
499
516
|
const apiStatus = await Promise.race([observedApiStatus, Promise.resolve(undefined)]);
|
|
500
517
|
const apiNote = apiStatus === undefined ? 'sayfanın veri isteği gözlenmedi' : `veri isteği HTTP ${apiStatus}`;
|
|
501
518
|
if (readiness.state === 'not-ready' && request.captureUnready !== true) {
|
|
502
|
-
|
|
519
|
+
const failure = describePageFailure(consoleLog, await readRenderErrors(page));
|
|
520
|
+
throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}.${failure} Bekleme bütçesi ${readyBudgetMs} ms; sayfa yalnız yavaşsa delayMs ile büyütün. Sayfa gerçekten bozuksa page_runtime_get ile şemayı, STUDIO_BASE_URL ve slug'ları doğrulayın. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
|
|
503
521
|
}
|
|
504
522
|
await page.evaluate('document.fonts.ready.then(() => true)').catch(() => undefined);
|
|
505
523
|
const flowsSettled = readiness.state === 'ready' || readiness.state === 'ready-no-flows'
|
|
@@ -507,7 +525,7 @@ export class StudioScreenshotter {
|
|
|
507
525
|
: readiness.state === 'not-ready' ? false : undefined;
|
|
508
526
|
const finalHref = readiness.snapshot?.href;
|
|
509
527
|
const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
|
|
510
|
-
const actions = await this.runActions(page, request);
|
|
528
|
+
const actions = await this.runActions(page, request, consoleLog);
|
|
511
529
|
// Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
|
|
512
530
|
// boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
|
|
513
531
|
// tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
|
|
@@ -548,6 +566,9 @@ export class StudioScreenshotter {
|
|
|
548
566
|
const runtimeErrors = await page.evaluate(flowRuntimeErrorsScript)
|
|
549
567
|
.then(value => Array.isArray(value) ? value : [])
|
|
550
568
|
.catch(() => []);
|
|
569
|
+
const renderErrors = await readRenderErrors(page);
|
|
570
|
+
const consoleErrors = consoleLog.consoleErrors();
|
|
571
|
+
const pageErrors = consoleLog.pageErrors();
|
|
551
572
|
// Özet kareden ÖNCE ve büyütmeden SONRA: ikisi de aynı yerleşmiş sayfayı anlatsın.
|
|
552
573
|
const summaryRaw = request.summary === false
|
|
553
574
|
? undefined
|
|
@@ -590,6 +611,9 @@ export class StudioScreenshotter {
|
|
|
590
611
|
...(layout === undefined ? {} : { layout }),
|
|
591
612
|
...(actions === undefined ? {} : { actions }),
|
|
592
613
|
...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
|
|
614
|
+
...(consoleErrors.length > 0 ? { consoleErrors } : {}),
|
|
615
|
+
...(pageErrors.length > 0 ? { pageErrors } : {}),
|
|
616
|
+
...(renderErrors.length > 0 ? { renderErrors } : {}),
|
|
593
617
|
...(flowsSettled === undefined ? {} : { flowsSettled }),
|
|
594
618
|
...(navigationRetried ? { navigationRetried: true } : {})
|
|
595
619
|
};
|