@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.
Files changed (47) hide show
  1. package/.env.example +15 -0
  2. package/dist/config.js +9 -0
  3. package/dist/index.js +2 -0
  4. package/dist/knowledge/componentCatalog.js +1 -1
  5. package/dist/knowledge/mobileComponentCatalog.js +231 -58
  6. package/dist/knowledge/mobileComponentEvents.js +118 -3
  7. package/dist/knowledge/normalize.js +16 -6
  8. package/dist/knowledge/platform/lint.js +110 -3
  9. package/dist/knowledge/platform/mobileRules.js +64 -8
  10. package/dist/knowledge/rules.js +56 -7
  11. package/dist/knowledge/styleResolve.js +49 -3
  12. package/dist/knowledge/styleResolveNative.js +104 -4
  13. package/dist/observability/trace.js +1 -1
  14. package/dist/platform/argumentAliases.js +128 -0
  15. package/dist/platform/bodyFile.js +103 -0
  16. package/dist/platform/capture/capture.js +29 -5
  17. package/dist/platform/capture/console-log.js +80 -0
  18. package/dist/platform/capture/index.js +1 -0
  19. package/dist/platform/diagnostics.js +10 -4
  20. package/dist/platform/http.js +23 -7
  21. package/dist/platform/responses.js +6 -2
  22. package/dist/platform/schemaReadMemory.js +60 -0
  23. package/dist/platform/secrets.js +9 -0
  24. package/dist/platform/strictArguments.js +68 -12
  25. package/dist/platform/tokenVault.js +187 -4
  26. package/dist/resources/guides/expressions.md +1 -1
  27. package/dist/resources/guides/microservice.md +9 -2
  28. package/dist/resources/guides/middleware.md +5 -1
  29. package/dist/resources/guides/mobile.md +150 -14
  30. package/dist/resources/guides/page.md +146 -39
  31. package/dist/resources/guides/theme.md +4 -1
  32. package/dist/resources/guides/workflow.md +7 -1
  33. package/dist/resources/schema-fingerprint.json +2 -2
  34. package/dist/server.js +1 -1
  35. package/dist/tools/datasources.js +1 -1
  36. package/dist/tools/designSchemaPatch.js +4 -3
  37. package/dist/tools/externalComponents.js +1 -1
  38. package/dist/tools/flows.js +70 -4
  39. package/dist/tools/layouts.js +5 -5
  40. package/dist/tools/microservices.js +26 -10
  41. package/dist/tools/pages.js +192 -36
  42. package/dist/tools/platformInfo.js +4 -1
  43. package/dist/tools/rows.js +7 -1
  44. package/dist/tools/schemaOps.js +1 -1
  45. package/dist/tools/screenWrite.js +4 -3
  46. package/dist/tools/screens.js +81 -0
  47. 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
- if (lift !== undefined)
255
- rules.transform = `translateY(calc(-1 * ${lift}))`;
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: vars['--ng-font'] ?? tokens.fontFamily,
218
- headingFamily: vars['--ng-font-heading'] ?? tokens.fontFamily,
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 = parseFontFamily(asText(typography.fontFamily, theme));
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
- 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)}. 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);
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
- throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}. 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);
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
  };